From 03c1cec48fa07d47af69f9c00cc799a2f4d644bd Mon Sep 17 00:00:00 2001 From: Nastya Date: Wed, 30 Apr 2025 20:59:50 +0300 Subject: [PATCH] init --- lib/api/hooks.ts | 2 +- lib/api/quizRelase.ts | 11 +++- lib/api/useQuizGetNext.ts | 32 ++++++++++ lib/components/QuizAnswerer.tsx | 58 +++++++++++++++--- .../ViewPublicationPage/ApologyPage.tsx | 1 + lib/components/ViewPublicationPage/Footer.tsx | 4 +- lib/contexts/QuizDataContext.ts | 19 +++--- lib/utils/hooks/useQuestionFlowControl.ts | 61 +++++++++++++------ 8 files changed, 144 insertions(+), 44 deletions(-) create mode 100644 lib/api/useQuizGetNext.ts diff --git a/lib/api/hooks.ts b/lib/api/hooks.ts index e54c8c1..5550583 100644 --- a/lib/api/hooks.ts +++ b/lib/api/hooks.ts @@ -2,7 +2,7 @@ import useSWR from "swr"; import { getQuizData } from "./quizRelase"; export function useQuizData(quizId: string, preview: boolean = false) { - return useSWR(preview ? null : ["quizData", quizId], (params) => getQuizData(params[1]), { + return useSWR(preview ? null : ["quizData", quizId], (params) => getQuizData({ quizId: params[1] }), { revalidateOnFocus: false, revalidateOnReconnect: false, shouldRetryOnError: false, diff --git a/lib/api/quizRelase.ts b/lib/api/quizRelase.ts index de0e77e..fbc61ba 100644 --- a/lib/api/quizRelase.ts +++ b/lib/api/quizRelase.ts @@ -120,7 +120,15 @@ export async function getData(quizId: string): Promise<{ } } -export async function getQuizData(quizId: string, status?: string): Promise { +export async function getQuizData({ + quizId, + status, + type = "", +}: { + quizId: string; + status?: string; + type?: string; +}): Promise { let maxRetries = 50; if (!quizId) throw new Error("No quiz id"); @@ -197,7 +205,6 @@ export async function getQuizData(quizId: string, status?: string): Promise { + const { addQuestion, quizId, settings } = useQuizSettings(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + + const loadMoreQuestions = async () => { + setIsLoading(true); + setError(null); + + try { + const data = await getQuizData({ quizId, type: settings.cfg.type || "", status: settings.status }); + const newQuestion = data?.questions[0]; + if (newQuestion) { + newQuestion.page = currentPage; + addQuestion(newQuestion); + setCurrentPage((old) => old++); + return newQuestion; + } + } catch (err) { + setError(err as Error); + } finally { + setIsLoading(false); + } + }; + + return { loadMoreQuestions, isLoading, error, currentPage }; +}; diff --git a/lib/components/QuizAnswerer.tsx b/lib/components/QuizAnswerer.tsx index c733d39..8525307 100644 --- a/lib/components/QuizAnswerer.tsx +++ b/lib/components/QuizAnswerer.tsx @@ -3,7 +3,7 @@ import { QuizViewContext, createQuizViewStore } from "@/stores/quizView"; import LoadingSkeleton from "@/ui_kit/LoadingSkeleton"; import { useVkMetricsGoals } from "@/utils/hooks/metrics/useVkMetricsGoals"; import { useYandexMetricsGoals } from "@/utils/hooks/metrics/useYandexMetricsGoals"; -import { QuizSettingsContext } from "@contexts/QuizDataContext"; +import { QuizSettingsContext, QuizSettingsContextValue } from "@contexts/QuizDataContext"; import { RootContainerWidthContext } from "@contexts/RootContainerWidthContext"; import type { QuizSettings } from "@model/settingsData"; import { Box, CssBaseline, ScopedCssBaseline, ThemeProvider } from "@mui/material"; @@ -14,13 +14,15 @@ import { handleComponentError } from "@utils/handleComponentError"; import lightTheme from "@utils/themes/light"; import moment from "moment"; import { SnackbarProvider } from "notistack"; -import { startTransition, useEffect, useLayoutEffect, useRef, useState } from "react"; +import { startTransition, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { ErrorBoundary } from "react-error-boundary"; import { ApologyPage } from "./ViewPublicationPage/ApologyPage"; import ViewPublicationPage from "./ViewPublicationPage/ViewPublicationPage"; import { HelmetProvider } from "react-helmet-async"; import "moment/dist/locale/ru"; +import { AnyTypedQuizQuestion } from ".."; +import { produce } from "immer"; moment.locale("ru"); const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeText; @@ -32,7 +34,16 @@ type Props = { className?: string; disableGlobalCss?: boolean; }; - +function isQuizSettingsValid(data: any): data is QuizSettings { + return ( + data && + Array.isArray(data.questions) && + data.settings && + typeof data.cnt === "number" && + typeof data.recentlyCompleted === "boolean" && + typeof data.show_badge === "boolean" + ); +} function QuizAnswererInner({ quizSettings, quizId, @@ -47,6 +58,14 @@ function QuizAnswererInner({ const { data, error, isLoading } = useQuizData(quizId, preview); const vkMetrics = useVkMetricsGoals(quizSettings?.settings.cfg.vkMetricsNumber); const yandexMetrics = useYandexMetricsGoals(quizSettings?.settings.cfg.yandexMetricsNumber); + const [localQuizSettings, setLocalQuizSettings] = useState(quizSettings); + + // Добавляем эффект для обновления localQuizSettings при получении новых данных + useEffect(() => { + if (data && !quizSettings) { + setLocalQuizSettings(data); + } + }, [data, quizSettings]); useEffect(() => { setTimeout(() => { @@ -73,15 +92,36 @@ function QuizAnswererInner({ }; }, []); + const finalQuizSettings = quizSettings || localQuizSettings; + + const contextValue = useMemo( + () => ({ + ...(finalQuizSettings as QuizSettings), + quizId, + preview, + changeFaviconAndTitle, + addQuestion: (newQuestion: AnyTypedQuizQuestion) => { + setLocalQuizSettings((prev) => { + if (!prev) return prev; + + return produce(prev, (draft) => { + draft.questions.push(newQuestion); + }); + }); + }, + }), + [quizId, preview, changeFaviconAndTitle, finalQuizSettings] + ); + if (isLoading) return ; if (error) return ; - // if (!data) return ; - quizSettings ??= data; - if (!quizSettings) return ; - if (quizSettings.questions.length === 1 && quizSettings?.settings.cfg.noStartPage) + if (!finalQuizSettings) return ; + if (!finalQuizSettings.questions || finalQuizSettings.questions.length === 0) + return ; + + if (finalQuizSettings.questions.length === 1 && finalQuizSettings?.settings.cfg.noStartPage) return ; - // if (quizSettings.questions.length === 1) return ; if (!quizId) return ; const quizContainer = ( @@ -106,7 +146,7 @@ function QuizAnswererInner({ return ( - + {disableGlobalCss ? ( ; export const ApologyPage = ({ error }: Props) => { + console.log(error); let message = "Что-то пошло не так"; if (error.response?.data === "quiz is inactive") message = "Квиз не активирован"; diff --git a/lib/components/ViewPublicationPage/Footer.tsx b/lib/components/ViewPublicationPage/Footer.tsx index c2be2c6..8d7c8b0 100644 --- a/lib/components/ViewPublicationPage/Footer.tsx +++ b/lib/components/ViewPublicationPage/Footer.tsx @@ -13,7 +13,7 @@ type FooterProps = { export const Footer = ({ stepNumber, nextButton, prevButton }: FooterProps) => { const theme = useTheme(); - const { questions, settings } = useQuizSettings(); + const { questions, settings, cnt } = useQuizSettings(); const questionsAmount = questions.filter(({ type }) => type !== "result").length; return ( @@ -41,7 +41,7 @@ export const Footer = ({ stepNumber, nextButton, prevButton }: FooterProps) => { {stepNumber !== null && settings.status !== "ai" && ( - Вопрос {stepNumber} из {questionsAmount} + Вопрос {stepNumber} из {cnt} (null); +export type QuizSettingsContextValue = QuizSettings & { + quizId: string; + preview: boolean; + changeFaviconAndTitle: boolean; + addQuestion: (newQuestion: AnyTypedQuizQuestion) => void; +}; + +export const QuizSettingsContext = createContext(null); export const useQuizSettings = () => { const quizSettings = useContext(QuizSettingsContext); if (quizSettings === null) throw new Error("QuizSettings context is null"); - return quizSettings; }; diff --git a/lib/utils/hooks/useQuestionFlowControl.ts b/lib/utils/hooks/useQuestionFlowControl.ts index 18cc9fe..fccb8e9 100644 --- a/lib/utils/hooks/useQuestionFlowControl.ts +++ b/lib/utils/hooks/useQuestionFlowControl.ts @@ -11,17 +11,16 @@ import { useVkMetricsGoals } from "@/utils/hooks/metrics/useVkMetricsGoals"; import { useYandexMetricsGoals } from "@/utils/hooks/metrics/useYandexMetricsGoals"; import { AnyTypedQuizQuestion } from "@/index"; import { getQuizData } from "@/api/quizRelase"; +import { useQuizGetNext } from "@/api/useQuizGetNext"; let isgetting = false; export function useQuestionFlowControl() { //Получаем инфо о квизе и список вопросов. - const { settings, questions: initialQuestions, quizId } = useQuizSettings(); - const [questions, setQuestions] = useState(initialQuestions); + const { loadMoreQuestions } = useQuizGetNext(); + const { settings, questions, quizId, cnt } = useQuizSettings(); - const addQuestion = (question: AnyTypedQuizQuestion) => { - setQuestions((prev) => [...prev, question]); - }; + console.log(questions); //Когда квиз линейный, не ветвящийся, мы идём по вопросам по их порядковому номеру. Это их page. //За корректность page отвечает конструктор квизов. Интересный факт, если в конструкторе удалить из середины вопрос, то случится куча запросов изменения вопросов с изменением этого page @@ -43,7 +42,7 @@ export function useQuestionFlowControl() { //Изменение стейта (переменной currentQuestionId) ведёт к пересчёту что же за объект сейчас используется. Мы каждый раз просто ищем в списке const currentQuestion = sortedQuestions.find((question) => question.id === currentQuestionId) ?? sortedQuestions[0]; - // console.log(currentQuestion) + console.log(currentQuestion); //Индекс текущего вопроса только если квиз линейный const linearQuestionIndex = //: number | null @@ -123,7 +122,7 @@ export function useQuestionFlowControl() { return nextQuestionIdPointsLogic(); } return nextQuestionIdMainLogic(); - }, [nextQuestionIdMainLogic, nextQuestionIdPointsLogic, settings.cfg.score]); + }, [nextQuestionIdMainLogic, nextQuestionIdPointsLogic, settings.cfg.score, questions]); //Поиск предыдущго вопроса либо по индексу либо по id родителя const prevQuestion = @@ -211,21 +210,39 @@ export function useQuestionFlowControl() { //рычаг управления из визуала в эту функцию const moveToNextQuestion = useCallback(async () => { - if (isgetting) return; - isgetting = true; - const data = await getQuizData(quizId, settings.status); - addQuestion(data.questions[0]); - isgetting = false; - if (!nextQuestion) throw new Error("Next question not found"); + // Если есть следующий вопрос в уже загруженных - используем его + if (nextQuestion) { + vkMetrics.questionPassed(currentQuestion.id); + yandexMetrics.questionPassed(currentQuestion.id); - // Засчитываем переход с вопроса дальше - vkMetrics.questionPassed(currentQuestion.id); - yandexMetrics.questionPassed(currentQuestion.id); + if (nextQuestion.type === "result") return showResult(); + setCurrentQuestionId(nextQuestion.id); + return; + } - if (nextQuestion.type === "result") return showResult(); - - setCurrentQuestionId(nextQuestion.id); - }, [currentQuestion.id, nextQuestion, showResult, vkMetrics, yandexMetrics]); + // Если следующего нет - загружаем новый + try { + const newQuestion = await loadMoreQuestions(); + if (newQuestion) { + vkMetrics.questionPassed(currentQuestion.id); + yandexMetrics.questionPassed(currentQuestion.id); + console.log("МЫ ПАЛУЧИЛИ НОВЫЙ ВОПРОС"); + console.log(newQuestion); + setCurrentQuestionId(newQuestion.id); + } + } catch (error) { + enqueueSnackbar("Ошибка загрузки следующего вопроса"); + } + }, [ + currentQuestion.id, + nextQuestion, + showResult, + vkMetrics, + yandexMetrics, + linearQuestionIndex, + loadMoreQuestions, + questions, + ]); //рычаг управления из визуала в эту функцию const setQuestion = useCallback( @@ -249,6 +266,10 @@ export function useQuestionFlowControl() { 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]);