This commit is contained in:
Nastya 2025-04-30 20:59:50 +03:00
parent 1f2880f719
commit 03c1cec48f
8 changed files with 144 additions and 44 deletions

@ -2,7 +2,7 @@ import useSWR from "swr";
import { getQuizData } from "./quizRelase"; import { getQuizData } from "./quizRelase";
export function useQuizData(quizId: string, preview: boolean = false) { 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, revalidateOnFocus: false,
revalidateOnReconnect: false, revalidateOnReconnect: false,
shouldRetryOnError: false, shouldRetryOnError: false,

@ -120,7 +120,15 @@ export async function getData(quizId: string): Promise<{
} }
} }
export async function getQuizData(quizId: string, status?: string): Promise<QuizSettings> { export async function getQuizData({
quizId,
status,
type = "",
}: {
quizId: string;
status?: string;
type?: string;
}): Promise<QuizSettings> {
let maxRetries = 50; let maxRetries = 50;
if (!quizId) throw new Error("No quiz id"); if (!quizId) throw new Error("No quiz id");
@ -197,7 +205,6 @@ export async function getQuizData(quizId: string, status?: string): Promise<Quiz
).data as QuizSettings; ).data as QuizSettings;
res.recentlyCompleted = responseData.isRecentlyCompleted; res.recentlyCompleted = responseData.isRecentlyCompleted;
return res; return res;
} }

32
lib/api/useQuizGetNext.ts Normal file

@ -0,0 +1,32 @@
import { useQuizSettings } from "@/contexts/QuizDataContext";
import { useState } from "react";
import { getQuizData } from "./quizRelase";
export const useQuizGetNext = () => {
const { addQuestion, quizId, settings } = useQuizSettings();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(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 };
};

@ -3,7 +3,7 @@ import { QuizViewContext, createQuizViewStore } from "@/stores/quizView";
import LoadingSkeleton from "@/ui_kit/LoadingSkeleton"; import LoadingSkeleton from "@/ui_kit/LoadingSkeleton";
import { useVkMetricsGoals } from "@/utils/hooks/metrics/useVkMetricsGoals"; import { useVkMetricsGoals } from "@/utils/hooks/metrics/useVkMetricsGoals";
import { useYandexMetricsGoals } from "@/utils/hooks/metrics/useYandexMetricsGoals"; import { useYandexMetricsGoals } from "@/utils/hooks/metrics/useYandexMetricsGoals";
import { QuizSettingsContext } from "@contexts/QuizDataContext"; import { QuizSettingsContext, QuizSettingsContextValue } from "@contexts/QuizDataContext";
import { RootContainerWidthContext } from "@contexts/RootContainerWidthContext"; import { RootContainerWidthContext } from "@contexts/RootContainerWidthContext";
import type { QuizSettings } from "@model/settingsData"; import type { QuizSettings } from "@model/settingsData";
import { Box, CssBaseline, ScopedCssBaseline, ThemeProvider } from "@mui/material"; import { Box, CssBaseline, ScopedCssBaseline, ThemeProvider } from "@mui/material";
@ -14,13 +14,15 @@ import { handleComponentError } from "@utils/handleComponentError";
import lightTheme from "@utils/themes/light"; import lightTheme from "@utils/themes/light";
import moment from "moment"; import moment from "moment";
import { SnackbarProvider } from "notistack"; 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 { ErrorBoundary } from "react-error-boundary";
import { ApologyPage } from "./ViewPublicationPage/ApologyPage"; import { ApologyPage } from "./ViewPublicationPage/ApologyPage";
import ViewPublicationPage from "./ViewPublicationPage/ViewPublicationPage"; import ViewPublicationPage from "./ViewPublicationPage/ViewPublicationPage";
import { HelmetProvider } from "react-helmet-async"; import { HelmetProvider } from "react-helmet-async";
import "moment/dist/locale/ru"; import "moment/dist/locale/ru";
import { AnyTypedQuizQuestion } from "..";
import { produce } from "immer";
moment.locale("ru"); moment.locale("ru");
const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeText; const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeText;
@ -32,7 +34,16 @@ type Props = {
className?: string; className?: string;
disableGlobalCss?: boolean; 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({ function QuizAnswererInner({
quizSettings, quizSettings,
quizId, quizId,
@ -47,6 +58,14 @@ function QuizAnswererInner({
const { data, error, isLoading } = useQuizData(quizId, preview); const { data, error, isLoading } = useQuizData(quizId, preview);
const vkMetrics = useVkMetricsGoals(quizSettings?.settings.cfg.vkMetricsNumber); const vkMetrics = useVkMetricsGoals(quizSettings?.settings.cfg.vkMetricsNumber);
const yandexMetrics = useYandexMetricsGoals(quizSettings?.settings.cfg.yandexMetricsNumber); const yandexMetrics = useYandexMetricsGoals(quizSettings?.settings.cfg.yandexMetricsNumber);
const [localQuizSettings, setLocalQuizSettings] = useState(quizSettings);
// Добавляем эффект для обновления localQuizSettings при получении новых данных
useEffect(() => {
if (data && !quizSettings) {
setLocalQuizSettings(data);
}
}, [data, quizSettings]);
useEffect(() => { useEffect(() => {
setTimeout(() => { 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 <LoadingSkeleton />; if (isLoading) return <LoadingSkeleton />;
if (error) return <ApologyPage error={error} />; if (error) return <ApologyPage error={error} />;
// if (!data) return <LoadingSkeleton />;
quizSettings ??= data;
if (!quizSettings) return <ApologyPage error={new Error("quiz data is null")} />;
if (quizSettings.questions.length === 1 && quizSettings?.settings.cfg.noStartPage) if (!finalQuizSettings) return <ApologyPage error={new Error("quiz data is null")} />;
if (!finalQuizSettings.questions || finalQuizSettings.questions.length === 0)
return <ApologyPage error={new Error("No questions found")} />;
if (finalQuizSettings.questions.length === 1 && finalQuizSettings?.settings.cfg.noStartPage)
return <ApologyPage error={new Error("quiz is empty")} />; return <ApologyPage error={new Error("quiz is empty")} />;
// if (quizSettings.questions.length === 1) return <ApologyPage error={new Error("no questions found")} />;
if (!quizId) return <ApologyPage error={new Error("no quiz id")} />; if (!quizId) return <ApologyPage error={new Error("no quiz id")} />;
const quizContainer = ( const quizContainer = (
@ -106,7 +146,7 @@ function QuizAnswererInner({
return ( return (
<QuizViewContext.Provider value={quizViewStore}> <QuizViewContext.Provider value={quizViewStore}>
<RootContainerWidthContext.Provider value={rootContainerWidth}> <RootContainerWidthContext.Provider value={rootContainerWidth}>
<QuizSettingsContext.Provider value={{ ...quizSettings, quizId, preview, changeFaviconAndTitle }}> <QuizSettingsContext.Provider value={contextValue}>
{disableGlobalCss ? ( {disableGlobalCss ? (
<ScopedCssBaseline <ScopedCssBaseline
sx={{ sx={{

@ -4,6 +4,7 @@ import { FallbackProps } from "react-error-boundary";
type Props = Partial<FallbackProps>; type Props = Partial<FallbackProps>;
export const ApologyPage = ({ error }: Props) => { export const ApologyPage = ({ error }: Props) => {
console.log(error);
let message = "Что-то пошло не так"; let message = "Что-то пошло не так";
if (error.response?.data === "quiz is inactive") message = "Квиз не активирован"; if (error.response?.data === "quiz is inactive") message = "Квиз не активирован";

@ -13,7 +13,7 @@ type FooterProps = {
export const Footer = ({ stepNumber, nextButton, prevButton }: FooterProps) => { export const Footer = ({ stepNumber, nextButton, prevButton }: FooterProps) => {
const theme = useTheme(); const theme = useTheme();
const { questions, settings } = useQuizSettings(); const { questions, settings, cnt } = useQuizSettings();
const questionsAmount = questions.filter(({ type }) => type !== "result").length; const questionsAmount = questions.filter(({ type }) => type !== "result").length;
return ( return (
@ -41,7 +41,7 @@ export const Footer = ({ stepNumber, nextButton, prevButton }: FooterProps) => {
{stepNumber !== null && settings.status !== "ai" && ( {stepNumber !== null && settings.status !== "ai" && (
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1 }}>
<Typography sx={{ color: theme.palette.text.primary }}> <Typography sx={{ color: theme.palette.text.primary }}>
Вопрос {stepNumber} из {questionsAmount} Вопрос {stepNumber} из {cnt}
</Typography> </Typography>
<Stepper <Stepper
activeStep={stepNumber} activeStep={stepNumber}

@ -1,19 +1,18 @@
import { QuizSettings } from "@model/settingsData"; import { QuizSettings } from "@model/settingsData";
import { createContext, useContext } from "react"; import { createContext, useContext, useMemo } from "react";
import { AnyTypedQuizQuestion } from ".."; import { AnyTypedQuizQuestion } from "..";
export const QuizSettingsContext = createContext< export type QuizSettingsContextValue = QuizSettings & {
| (QuizSettings & { quizId: string;
quizId: string; preview: boolean;
preview: boolean; changeFaviconAndTitle: boolean;
changeFaviconAndTitle: boolean; addQuestion: (newQuestion: AnyTypedQuizQuestion) => void;
}) };
| null
>(null); export const QuizSettingsContext = createContext<QuizSettingsContextValue | null>(null);
export const useQuizSettings = () => { export const useQuizSettings = () => {
const quizSettings = useContext(QuizSettingsContext); const quizSettings = useContext(QuizSettingsContext);
if (quizSettings === null) throw new Error("QuizSettings context is null"); if (quizSettings === null) throw new Error("QuizSettings context is null");
return quizSettings; return quizSettings;
}; };

@ -11,17 +11,16 @@ import { useVkMetricsGoals } from "@/utils/hooks/metrics/useVkMetricsGoals";
import { useYandexMetricsGoals } from "@/utils/hooks/metrics/useYandexMetricsGoals"; import { useYandexMetricsGoals } from "@/utils/hooks/metrics/useYandexMetricsGoals";
import { AnyTypedQuizQuestion } from "@/index"; import { AnyTypedQuizQuestion } from "@/index";
import { getQuizData } from "@/api/quizRelase"; import { getQuizData } from "@/api/quizRelase";
import { useQuizGetNext } from "@/api/useQuizGetNext";
let isgetting = false; let isgetting = false;
export function useQuestionFlowControl() { export function useQuestionFlowControl() {
//Получаем инфо о квизе и список вопросов. //Получаем инфо о квизе и список вопросов.
const { settings, questions: initialQuestions, quizId } = useQuizSettings(); const { loadMoreQuestions } = useQuizGetNext();
const [questions, setQuestions] = useState(initialQuestions); const { settings, questions, quizId, cnt } = useQuizSettings();
const addQuestion = (question: AnyTypedQuizQuestion) => { console.log(questions);
setQuestions((prev) => [...prev, question]);
};
//Когда квиз линейный, не ветвящийся, мы идём по вопросам по их порядковому номеру. Это их page. //Когда квиз линейный, не ветвящийся, мы идём по вопросам по их порядковому номеру. Это их page.
//За корректность page отвечает конструктор квизов. Интересный факт, если в конструкторе удалить из середины вопрос, то случится куча запросов изменения вопросов с изменением этого page //За корректность page отвечает конструктор квизов. Интересный факт, если в конструкторе удалить из середины вопрос, то случится куча запросов изменения вопросов с изменением этого page
@ -43,7 +42,7 @@ export function useQuestionFlowControl() {
//Изменение стейта (переменной currentQuestionId) ведёт к пересчёту что же за объект сейчас используется. Мы каждый раз просто ищем в списке //Изменение стейта (переменной currentQuestionId) ведёт к пересчёту что же за объект сейчас используется. Мы каждый раз просто ищем в списке
const currentQuestion = sortedQuestions.find((question) => question.id === currentQuestionId) ?? sortedQuestions[0]; const currentQuestion = sortedQuestions.find((question) => question.id === currentQuestionId) ?? sortedQuestions[0];
// console.log(currentQuestion) console.log(currentQuestion);
//Индекс текущего вопроса только если квиз линейный //Индекс текущего вопроса только если квиз линейный
const linearQuestionIndex = //: number | null const linearQuestionIndex = //: number | null
@ -123,7 +122,7 @@ export function useQuestionFlowControl() {
return nextQuestionIdPointsLogic(); return nextQuestionIdPointsLogic();
} }
return nextQuestionIdMainLogic(); return nextQuestionIdMainLogic();
}, [nextQuestionIdMainLogic, nextQuestionIdPointsLogic, settings.cfg.score]); }, [nextQuestionIdMainLogic, nextQuestionIdPointsLogic, settings.cfg.score, questions]);
//Поиск предыдущго вопроса либо по индексу либо по id родителя //Поиск предыдущго вопроса либо по индексу либо по id родителя
const prevQuestion = const prevQuestion =
@ -211,21 +210,39 @@ export function useQuestionFlowControl() {
//рычаг управления из визуала в эту функцию //рычаг управления из визуала в эту функцию
const moveToNextQuestion = useCallback(async () => { const moveToNextQuestion = useCallback(async () => {
if (isgetting) return; // Если есть следующий вопрос в уже загруженных - используем его
isgetting = true; if (nextQuestion) {
const data = await getQuizData(quizId, settings.status); vkMetrics.questionPassed(currentQuestion.id);
addQuestion(data.questions[0]); yandexMetrics.questionPassed(currentQuestion.id);
isgetting = false;
if (!nextQuestion) throw new Error("Next question not found");
// Засчитываем переход с вопроса дальше if (nextQuestion.type === "result") return showResult();
vkMetrics.questionPassed(currentQuestion.id); setCurrentQuestionId(nextQuestion.id);
yandexMetrics.questionPassed(currentQuestion.id); return;
}
if (nextQuestion.type === "result") return showResult(); // Если следующего нет - загружаем новый
try {
setCurrentQuestionId(nextQuestion.id); const newQuestion = await loadMoreQuestions();
}, [currentQuestion.id, nextQuestion, showResult, vkMetrics, yandexMetrics]); 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( const setQuestion = useCallback(
@ -249,6 +266,10 @@ export function useQuestionFlowControl() {
return hasAnswer; return hasAnswer;
} }
console.log(linearQuestionIndex);
console.log(questions.length);
console.log(cnt);
if (linearQuestionIndex !== null && questions.length < cnt) return true;
return Boolean(nextQuestion); return Boolean(nextQuestion);
}, [answers, currentQuestion, nextQuestion]); }, [answers, currentQuestion, nextQuestion]);