refactor component hierarchy

move logic to dedicated hook
use quizId from context, not from settings
add ErrorBoundaryFallback component
This commit is contained in:
nflnkr 2024-02-08 16:42:31 +03:00
parent ba547dcb55
commit e4d9018962
30 changed files with 685 additions and 793 deletions

@ -1,20 +1,22 @@
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import { useEffect, useState } from "react"; import { startTransition, useEffect, useState } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import QuizAnswerer from "./QuizAnswerer"; import QuizAnswerer from "./QuizAnswerer";
import { QuizIdContext } from "./contexts/QuizIdContext"; import { QuizIdContext } from "./contexts/QuizIdContext";
import { RootContainerWidthContext } from "./contexts/RootContainerWidthContext"; import { RootContainerWidthContext } from "./contexts/RootContainerWidthContext";
const defaultQuizId = "ef836ff8-35b1-4031-9acf-af5766bac2b2"; const defaultQuizId = "45ef7f9c-784d-4e58-badb-f6b337f08ba0";
export default function App() { export default function App() {
const quizId = useParams().quizId ?? defaultQuizId; const quizId = useParams().quizId ?? defaultQuizId;
const [rootContainerSize, setRootContainerSize] = useState<number>(Infinity); const [rootContainerSize, setRootContainerSize] = useState<number>(() => window.innerWidth);
useEffect(() => { useEffect(() => {
const handleWindowResize = () => { const handleWindowResize = () => {
setRootContainerSize(window.innerWidth); startTransition(() => {
setRootContainerSize(window.innerWidth);
});
}; };
window.addEventListener("resize", handleWindowResize); window.addEventListener("resize", handleWindowResize);

@ -2,6 +2,7 @@ import { CssBaseline, ThemeProvider } from "@mui/material";
import { LocalizationProvider } from "@mui/x-date-pickers"; import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment"; import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
import { ruRU } from '@mui/x-date-pickers/locales'; import { ruRU } from '@mui/x-date-pickers/locales';
import ErrorBoundaryFallback from "@ui_kit/ErrorBoundaryFallback";
import LoadingSkeleton from "@ui_kit/LoadingSkeleton"; import LoadingSkeleton from "@ui_kit/LoadingSkeleton";
import { handleComponentError } from "@utils/handleComponentError"; import { handleComponentError } from "@utils/handleComponentError";
import moment from "moment"; import moment from "moment";
@ -9,8 +10,7 @@ import { SnackbarProvider } from 'notistack';
import { Suspense } from "react"; import { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary";
import { SWRConfig } from "swr"; import { SWRConfig } from "swr";
import { ApologyPage } from "./pages/ViewPublicationPage/ApologyPage"; import ViewPublicationPage from "./pages/ViewPublicationPage/ViewPublicationPage";
import { ViewPage } from "./pages/ViewPublicationPage/ViewPublicationPage";
import lightTheme from "./utils/themes/light"; import lightTheme from "./utils/themes/light";
@ -32,11 +32,11 @@ export default function QuizAnswerer() {
> >
<CssBaseline /> <CssBaseline />
<ErrorBoundary <ErrorBoundary
fallback={<ApologyPage message="Что-то пошло не так" />} FallbackComponent={ErrorBoundaryFallback}
onError={handleComponentError} onError={handleComponentError}
> >
<Suspense fallback={<LoadingSkeleton />}> <Suspense fallback={<LoadingSkeleton />}>
<ViewPage /> <ViewPublicationPage />
</Suspense> </Suspense>
</ErrorBoundary> </ErrorBoundary>
</SnackbarProvider> </SnackbarProvider>

@ -1,5 +1,5 @@
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import { useEffect, useRef, useState } from "react"; import { startTransition, useEffect, useRef, useState } from "react";
import QuizAnswerer from "./QuizAnswerer"; import QuizAnswerer from "./QuizAnswerer";
import { QuizIdContext } from "./contexts/QuizIdContext"; import { QuizIdContext } from "./contexts/QuizIdContext";
import { RootContainerWidthContext } from "./contexts/RootContainerWidthContext"; import { RootContainerWidthContext } from "./contexts/RootContainerWidthContext";
@ -10,14 +10,14 @@ interface Props {
} }
export default function WidgetApp({ quizId }: Props) { export default function WidgetApp({ quizId }: Props) {
const [rootContainerSize, setRootContainerSize] = useState<number>(Infinity); const [rootContainerSize, setRootContainerSize] = useState<number>(() => window.innerWidth);
const rootContainerRef = useRef<HTMLDivElement>(null); const rootContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
const handleWindowResize = () => { const handleWindowResize = () => {
if (!rootContainerRef.current) return; startTransition(() => {
if (rootContainerRef.current) setRootContainerSize(rootContainerRef.current.clientWidth);
setRootContainerSize(rootContainerRef.current.clientWidth); });
}; };
window.addEventListener("resize", handleWindowResize); window.addEventListener("resize", handleWindowResize);

@ -6,143 +6,124 @@ import type { AxiosError } from "axios";
let SESSIONS = ""; let SESSIONS = "";
export const publicationMakeRequest = ({ url, body }: any) => { export const publicationMakeRequest = ({ url, body }: any) => {
console.log(body); console.log(body);
return axios(url, { return axios(url, {
//@ts-ignore data: body,
data: body, headers: {
headers: { "X-Sessionkey": SESSIONS,
"X-Sessionkey": SESSIONS, "Content-Type": "multipart/form-data",
"Content-Type": "multipart/form-data", },
}, method: "POST",
method: "POST", });
});
}; };
export async function getData(quizId: string): Promise<{ export async function getData(quizId: string): Promise<{
data: GetQuizDataResponse | null; data: GetQuizDataResponse | null;
isRecentlyCompleted: boolean; isRecentlyCompleted: boolean;
error?: string; error?: string;
}> { }> {
const QID = try {
process.env.NODE_ENV === "production" const { data, headers } = await axios<GetQuizDataResponse>(
? window.location.pathname.replace(/\//g, "") `https://s.hbpn.link/answer/settings`,
: "ef836ff8-35b1-4031-9acf-af5766bac2b2"; {
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: {
quiz_id: quizId,
limit: 100,
page: 0,
need_config: true,
},
}
);
const sessions = JSON.parse(localStorage.getItem("sessions") || "{}");
try { if (typeof sessions[quizId] === "number") {
const { data, headers } = await axios<GetQuizDataResponse>( // unix время. Если меньше суток прошло - выводить ошибку, иначе пустить дальше
`https://s.hbpn.link/answer/settings`, if (Date.now() - sessions[quizId] < 86400000) {
{ return { data, isRecentlyCompleted: true };
method: "POST", }
// headers, }
data: JSON.stringify({
quiz_id: quizId,
limit: 100,
page: 0,
need_config: true,
}),
}
);
const sessions = JSON.parse(localStorage.getItem("sessions") || "{}");
if (typeof sessions[QID] === "number") { SESSIONS = headers["x-sessionkey"];
// unix время. Если меньше суток прошло - выводить ошибку, иначе пустить дальше
if (Date.now() - sessions[QID] < 86400000) { return { data, isRecentlyCompleted: false };
return { data, isRecentlyCompleted: true }; } catch (nativeError) {
} const error = nativeError as AxiosError;
return { data: null, isRecentlyCompleted: false, error: error.message };
} }
SESSIONS = headers["x-sessionkey"];
return { data, isRecentlyCompleted: false };
} catch (nativeError) {
const error = nativeError as AxiosError;
return { data: null, isRecentlyCompleted: false, error: error.message };
}
} }
export function sendAnswer({ questionId, body, qid }: any) { export function sendAnswer({ questionId, body, qid }: any) {
const formData = new FormData(); const formData = new FormData();
console.log(qid); console.log(qid);
const answers = [ const answers = [
{ {
question_id: questionId, question_id: questionId,
content: body, //тут массив с ответом content: body, //тут массив с ответом
}, },
]; ];
formData.append("answers", JSON.stringify(answers)); formData.append("answers", JSON.stringify(answers));
formData.append("qid", qid); formData.append("qid", qid);
return publicationMakeRequest({ return publicationMakeRequest({
url: `https://s.hbpn.link/answer/answer`, url: `https://s.hbpn.link/answer/answer`,
body: formData, body: formData,
method: "POST", method: "POST",
}); });
} }
//body ={file, filename} //body ={file, filename}
export function sendFile({ questionId, body, qid }: any) { export function sendFile({ questionId, body, qid }: any) {
console.log(body); console.log(body);
const formData = new FormData(); const formData = new FormData();
const answers: any = [ const answers: any = [
{ {
question_id: questionId, question_id: questionId,
content: "file:" + body.name, content: "file:" + body.name,
}, },
]; ];
formData.append("answers", JSON.stringify(answers)); formData.append("answers", JSON.stringify(answers));
formData.append(body.name, body.file); formData.append(body.name, body.file);
formData.append("qid", qid); formData.append("qid", qid);
return publicationMakeRequest({ return publicationMakeRequest({
url: `https://s.hbpn.link/answer/answer`, url: `https://s.hbpn.link/answer/answer`,
body: formData, body: formData,
method: "POST", method: "POST",
}); });
} }
const fields = [
"name",
"email",
"phone",
"adress",
"telegram",
"wechat",
"viber",
"vk",
"skype",
"whatsup",
"messenger",
"text",
];
//форма контактов //форма контактов
export function sendFC({ questionId, body, qid }: any) { export function sendFC({ questionId, body, qid }: any) {
const formData = new FormData(); const formData = new FormData();
// const keysBody = Object.keys(body) // const keysBody = Object.keys(body)
// const content:any = {} // const content:any = {}
// fields.forEach((key) => { // fields.forEach((key) => {
// if (keysBody.includes(key)) content[key] = body.key // if (keysBody.includes(key)) content[key] = body.key
// }) // })
const answers = [ const answers = [
{ {
question_id: questionId, question_id: questionId,
content: JSON.stringify(body), content: JSON.stringify(body),
result: true, result: true,
qid, qid,
}, },
]; ];
formData.append("answers", JSON.stringify(answers)); formData.append("answers", JSON.stringify(answers));
formData.append("qid", qid); formData.append("qid", qid);
return publicationMakeRequest({ return publicationMakeRequest({
url: `https://s.hbpn.link/answer/answer`, url: `https://s.hbpn.link/answer/answer`,
body: formData, body: formData,
method: "POST", method: "POST",
}); });
} }

@ -24,7 +24,7 @@ export interface GetQuizDataResponse {
}[]; }[];
} }
export function parseQuizData(quizDataResponse: GetQuizDataResponse, quizId: string): Omit<QuizSettings, "recentlyCompleted"> { export function parseQuizData(quizDataResponse: GetQuizDataResponse): Omit<QuizSettings, "recentlyCompleted"> {
const items: QuizSettings["questions"] = quizDataResponse.items.map((item) => { const items: QuizSettings["questions"] = quizDataResponse.items.map((item) => {
const content = JSON.parse(item.c); const content = JSON.parse(item.c);
@ -40,7 +40,6 @@ export function parseQuizData(quizDataResponse: GetQuizDataResponse, quizId: str
}); });
const settings: QuizSettings["settings"] = { const settings: QuizSettings["settings"] = {
qid: quizId,
fp: quizDataResponse.settings.fp, fp: quizDataResponse.settings.fp,
rep: quizDataResponse.settings.rep, rep: quizDataResponse.settings.rep,
name: quizDataResponse.settings.name, name: quizDataResponse.settings.name,

@ -1,3 +1,4 @@
import { nanoid } from "nanoid";
import type { QuizQuestionDate } from "./date"; import type { QuizQuestionDate } from "./date";
import type { QuizQuestionEmoji } from "./emoji"; import type { QuizQuestionEmoji } from "./emoji";
import type { QuizQuestionFile } from "./file"; import type { QuizQuestionFile } from "./file";
@ -5,24 +6,23 @@ import type { QuizQuestionImages } from "./images";
import type { QuizQuestionNumber } from "./number"; import type { QuizQuestionNumber } from "./number";
import type { QuizQuestionPage } from "./page"; import type { QuizQuestionPage } from "./page";
import type { QuizQuestionRating } from "./rating"; import type { QuizQuestionRating } from "./rating";
import type { QuizQuestionResult } from "./result";
import type { QuizQuestionSelect } from "./select"; import type { QuizQuestionSelect } from "./select";
import type { QuizQuestionText } from "./text"; import type { QuizQuestionText } from "./text";
import type { QuizQuestionVariant } from "./variant"; import type { QuizQuestionVariant } from "./variant";
import type { QuizQuestionVarImg } from "./varimg"; import type { QuizQuestionVarImg } from "./varimg";
import type { QuizQuestionResult } from "./result";
import { nanoid } from "nanoid";
export interface QuestionBranchingRuleMain { export interface QuestionBranchingRuleMain {
next: string; next: string;
or: boolean; or: boolean;
rules: { rules: {
question: string; //id родителя (пока что) question: string; //id родителя (пока что)
answers: string[] answers: string[];
}[] }[];
} }
export interface QuestionBranchingRule {
children: string[], export interface QuestionBranchingRule {
children: string[];
//список условий //список условий
main: QuestionBranchingRuleMain[]; main: QuestionBranchingRuleMain[];
parentId: string | null | "root"; parentId: string | null | "root";
@ -85,7 +85,6 @@ export interface QuizQuestionBase {
}; };
} }
export type AnyTypedQuizQuestion = export type AnyTypedQuizQuestion =
| QuizQuestionVariant | QuizQuestionVariant
| QuizQuestionImages | QuizQuestionImages
@ -100,7 +99,7 @@ export type AnyTypedQuizQuestion =
| QuizQuestionRating | QuizQuestionRating
| QuizQuestionResult; | QuizQuestionResult;
export type RealTypedQuizQuestion = Exclude<AnyTypedQuizQuestion, QuizQuestionResult>;
type FilterQuestionsWithVariants<T> = T extends { type FilterQuestionsWithVariants<T> = T extends {
content: { variants: QuestionVariant[]; }; content: { variants: QuestionVariant[]; };
@ -109,18 +108,19 @@ type FilterQuestionsWithVariants<T> = T extends {
export type QuizQuestionsWithVariants = FilterQuestionsWithVariants<AnyTypedQuizQuestion>; export type QuizQuestionsWithVariants = FilterQuestionsWithVariants<AnyTypedQuizQuestion>;
export const createBranchingRuleMain: (targetId:string, parentId:string) => QuestionBranchingRuleMain = (targetId, parentId) => ({ export const createBranchingRuleMain: (targetId: string, parentId: string) => QuestionBranchingRuleMain = (targetId, parentId) => ({
next: targetId, next: targetId,
or: false, or: false,
rules: [{ rules: [{
question: parentId, question: parentId,
answers: [] as string[], answers: [] as string[],
}] }]
}) });
export const createQuestionVariant: () => QuestionVariant = () => ({ export const createQuestionVariant: () => QuestionVariant = () => ({
id: nanoid(), id: nanoid(),
answer: "", answer: "",
extendedText: "", extendedText: "",
hints: "", hints: "",
originalImageUrl: "", originalImageUrl: "",
}); });

@ -8,6 +8,8 @@ export type QuizType = "quiz" | "form";
export type QuizResultsType = true | null; export type QuizResultsType = true | null;
export type QuizStep = "startpage" | "question" | "contactform";
export type QuizTheme = export type QuizTheme =
| "StandardTheme" | "StandardTheme"
| "StandardDarkTheme" | "StandardDarkTheme"
@ -32,7 +34,6 @@ export type FCField = {
export type QuizSettings = { export type QuizSettings = {
questions: AnyTypedQuizQuestion[]; questions: AnyTypedQuizQuestion[];
settings: { settings: {
qid: string;
fp: boolean; fp: boolean;
rep: boolean; rep: boolean;
name: string; name: string;

@ -1,22 +1,25 @@
import { Box, Typography } from "@mui/material"; import { Box, Typography } from "@mui/material";
export const ApologyPage = ({message}:{message: string}) => { type Props = {
message: string;
};
export const ApologyPage = ({ message }: Props) => {
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
height: "100vh" height: "100%",
}} }}
> >
<Typography <Typography
sx={{ sx={{
textAlign: "center" textAlign: "center"
}} }}
>{message || "что-то пошло не так"}</Typography> >{message || "Что-то пошло не так"}</Typography>
</Box> </Box>
);
};
)
}

@ -11,69 +11,25 @@ import { FC, useRef, useState } from "react";
import { sendFC } from "@api/quizRelase"; import { sendFC } from "@api/quizRelase";
import { NameplateLogo } from "@icons/NameplateLogo"; import { NameplateLogo } from "@icons/NameplateLogo";
import { QuizQuestionResult } from "@model/questionTypes/result"; import { QuizQuestionResult } from "@model/questionTypes/result";
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { useQuizData } from "@utils/hooks/useQuizData"; import { useQuizData } from "@utils/hooks/useQuizData";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useQuizId } from "../../contexts/QuizIdContext";
import { useRootContainerSize } from "../../contexts/RootContainerWidthContext"; import { useRootContainerSize } from "../../contexts/RootContainerWidthContext";
import { ApologyPage } from "./ApologyPage";
import { checkEmptyData } from "./tools/checkEmptyData";
const TextField = MuiTextField as unknown as FC<TextFieldProps>; // temporary fix ts(2590) const TextField = MuiTextField as unknown as FC<TextFieldProps>; // temporary fix ts(2590)
const EMAIL_REGEXP = /^(([^<>()[\].,:\s@"]+(\.[^<>()[\].,:\s@"]+)*)|(".+"))@(([^<>()[\].,:\s@"]+\.)+[^<>()[\].,:\s@"]{2,})$/iu; const EMAIL_REGEXP = /^(([^<>()[\].,:\s@"]+(\.[^<>()[\].,:\s@"]+)*)|(".+"))@(([^<>()[\].,:\s@"]+\.)+[^<>()[\].,:\s@"]{2,})$/iu;
type ContactFormProps = { type Props = {
currentQuestion: any; currentQuestion: AnyTypedQuizQuestion;
showResultForm: boolean; onShowResult: () => void;
setShowContactForm: (show: boolean) => void;
setShowResultForm: (show: boolean) => void;
}; };
const icons = [ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
{
type: "name",
icon: NameIcon,
defaultText: "Введите имя",
defaultTitle: "имя",
backendName: "name",
},
{
type: "email",
icon: EmailIcon,
defaultText: "Введите Email",
defaultTitle: "Email",
backendName: "email",
},
{
type: "phone",
icon: PhoneIcon,
defaultText: "Введите номер телефона",
defaultTitle: "номер телефона",
backendName: "phone",
},
{
type: "text",
icon: TextIcon,
defaultText: "Введите фамилию",
defaultTitle: "фамилию",
backendName: "adress",
},
{
type: "address",
icon: AddressIcon,
defaultText: "Введите адрес",
defaultTitle: "адрес",
backendName: "adress",
},
];
export const ContactForm = ({
currentQuestion,
showResultForm,
setShowContactForm,
setShowResultForm,
}: ContactFormProps) => {
const theme = useTheme(); const theme = useTheme();
const qid = useQuizId();
const { settings, questions } = useQuizData(); const { settings, questions } = useQuizData();
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
@ -87,78 +43,103 @@ export const ContactForm = ({
const [fire, setFire] = useState(false); const [fire, setFire] = useState(false);
const isMobile = useRootContainerSize() < 850; const isMobile = useRootContainerSize() < 850;
const followNextForm = () => { const resultQuestion = currentQuestion.type === "result"
setShowContactForm(false); ? currentQuestion
setShowResultForm(true); : questions.find((question): question is QuizQuestionResult => {
}; if (settings?.cfg.haveRoot) {
return (
question.type === "result" &&
question.content.rule.parentId === currentQuestion.content.id
);
} else {
return (
question.type === "result" && question.content.rule.parentId === "line"
);
}
});
//@ts-ignore if (!resultQuestion) throw new Error("Result question not found");
const resultQuestion: QuizQuestionResult = questions.find((question) => {
if (settings?.cfg.haveRoot) {
//ветвимся
return (
question.type === "result" &&
//@ts-ignore
question.content.rule.parentId === currentQuestion.content.id
);
} else {
// не ветвимся
return (
question.type === "result" && question.content.rule.parentId === "line"
);
}
});
const inputHC = async () => { const inputHC = async () => {
//@ts-ignore const FC = settings.cfg.formContact.fields || settings.cfg.formContact;
const FC = settings?.cfg.formContact.fields || settings?.cfg.formContact; const body = {} as any;
const body = {};
//@ts-ignore
if (name.length > 0) body.name = name; if (name.length > 0) body.name = name;
//@ts-ignore
if (email.length > 0) body.email = email; if (email.length > 0) body.email = email;
//@ts-ignore
if (phone.length > 0) body.phone = phone; if (phone.length > 0) body.phone = phone;
//@ts-ignore
if (adress.length > 0) body.address = adress; if (adress.length > 0) body.address = adress;
//@ts-ignore
if (text.length > 0) body.customs = { [FC.text.text || "Фамилия"]: text }; if (text.length > 0) body.customs = { [FC.text.text || "Фамилия"]: text };
if (Object.keys(body).length > 0) { if (Object.keys(body).length > 0) {
try { try {
await sendFC({ await sendFC({
questionId: resultQuestion?.id, questionId: currentQuestion.id,
body: body, body: body,
qid: settings.qid, qid,
}); });
const sessions = JSON.parse(localStorage.getItem("sessions") || "{}"); const sessions = JSON.parse(localStorage.getItem("sessions") || "{}");
localStorage.setItem( localStorage.setItem(
"sessions", "sessions",
JSON.stringify({ ...sessions, [settings.qid]: new Date().getTime() }) JSON.stringify({ ...sessions, [qid]: new Date().getTime() })
); );
} catch (e) { } catch (e) {
enqueueSnackbar("ответ не был засчитан"); enqueueSnackbar("ответ не был засчитан");
} }
} }
}; };
//@ts-ignore
let FCcopy: any = settings?.cfg.formContact.fields || settings?.cfg.formContact;
let filteredFC: any = {}; const FCcopy: any = settings.cfg.formContact.fields || settings.cfg.formContact;
for (let i in FCcopy) {
let field = FCcopy[i]; const filteredFC: any = {};
for (const i in FCcopy) {
const field = FCcopy[i];
console.log(filteredFC); console.log(filteredFC);
if (field.used) { if (field.used) {
filteredFC[i] = field; filteredFC[i] = field;
} }
} }
let isWide = Object.keys(filteredFC).length > 2; const isWide = Object.keys(filteredFC).length > 2;
if (!resultQuestion) async function handleShowResultsClick() {
return ( //@ts-ignore
<ApologyPage message="не получилось найти результат для этой ветки :(" /> const FC: any = settings.cfg.formContact.fields || settings.cfg.formContact;
); if (FC["email"].used !== EMAIL_REGEXP.test(email)) {
return enqueueSnackbar("введена некорректная почта");
}
//почта валидна
setFire(true);
if (fireOnce.current) {
if (
name.length === 0
&& email.length === 0
&& phone.length === 0
&& text.length === 0
&& adress.length === 0
) return enqueueSnackbar("Пожалуйста, заполните поля");
try {
await inputHC();
fireOnce.current = false;
const sessions: any = JSON.parse(
localStorage.getItem("sessions") || "{}"
);
sessions[qid] = Date.now();
localStorage.setItem(
"sessions",
JSON.stringify(sessions)
);
enqueueSnackbar("Данные успешно отправлены");
} catch (e) {
enqueueSnackbar("повторите попытку позже");
}
onShowResult();
}
setFire(false);
}
return ( return (
<Box <Box
@ -167,7 +148,7 @@ export const ContactForm = ({
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
height: "100vh", height: "100%",
overflow: "auto", overflow: "auto",
"&::-webkit-scrollbar": { "&::-webkit-scrollbar": {
width: "0", width: "0",
@ -204,10 +185,10 @@ export const ContactForm = ({
color: theme.palette.text.primary, color: theme.palette.text.primary,
}} }}
> >
{settings?.cfg.formContact.title || {settings.cfg.formContact.title ||
"Заполните форму, чтобы получить результаты теста"} "Заполните форму, чтобы получить результаты теста"}
</Typography> </Typography>
{settings?.cfg.formContact.desc && ( {settings.cfg.formContact.desc && (
<Typography <Typography
sx={{ sx={{
color: theme.palette.text.primary, color: theme.palette.text.primary,
@ -216,7 +197,7 @@ export const ContactForm = ({
fontSize: "18px", fontSize: "18px",
}} }}
> >
{settings?.cfg.formContact.desc} {settings.cfg.formContact.desc}
</Typography> </Typography>
)} )}
</Box> </Box>
@ -254,64 +235,13 @@ export const ContactForm = ({
{ {
// resultQuestion && // resultQuestion &&
// settings?.cfg.resultInfo.when === "after" && // settings.cfg.resultInfo.when === "after" &&
<Button <Button
disabled={!(ready && !fire)} disabled={!(ready && !fire)}
variant="contained" variant="contained"
onClick={async () => { onClick={handleShowResultsClick}
//@ts-ignore
const FC: any = settings?.cfg.formContact.fields || settings?.cfg.formContact;
if (FC["email"].used === EMAIL_REGEXP.test(email)) {
//почта валидна
setFire(true);
if (fireOnce.current) {
if (
name.length > 0 ||
email.length > 0 ||
phone.length > 0 ||
text.length > 0 ||
adress.length > 0
) {
try {
await inputHC();
fireOnce.current = false;
const QID =
process.env.NODE_ENV === "production"
? window.location.pathname.replace(/\//g, "")
: "ef836ff8-35b1-4031-9acf-af5766bac2b2";
const sessions: any = JSON.parse(
localStorage.getItem("sessions") || "{}"
);
sessions[QID] = Date.now();
localStorage.setItem(
"sessions",
JSON.stringify(sessions)
);
enqueueSnackbar("Данные успешно отправлены");
} catch (e) {
enqueueSnackbar("повторите попытку позже");
}
if (
settings?.cfg.resultInfo.showResultForm === "after" &&
!checkEmptyData({ resultData: resultQuestion })
) {
setShowContactForm(false);
setShowResultForm(true);
}
} else {
enqueueSnackbar("Пожалуйста, заполните поля");
}
}
setFire(false);
} else {
enqueueSnackbar("введена некорректная почта");
}
}}
> >
{settings?.cfg.formContact?.button || "Получить результаты"} {settings.cfg.formContact?.button || "Получить результаты"}
</Button> </Button>
} }
@ -391,7 +321,7 @@ const Inputs = ({
const { settings } = useQuizData(); const { settings } = useQuizData();
// @ts-ignore // @ts-ignore
const FC = settings?.cfg.formContact.fields || settings?.cfg.formContact; const FC = settings.cfg.formContact.fields || settings.cfg.formContact;
if (!FC) return null; if (!FC) return null;

@ -1,228 +1,17 @@
import { Box, Button, Typography, useTheme } from "@mui/material"; import { Box, Typography, useTheme } from "@mui/material";
import { useCallback, useMemo, useState } from "react"; import { ReactNode } from "react";
import { enqueueSnackbar } from "notistack";
import type { AnyTypedQuizQuestion, QuizQuestionBase } from "../../model/questionTypes/shared";
import { checkEmptyData } from "./tools/checkEmptyData";
import type { QuizQuestionResult } from "@model/questionTypes/result";
import { useQuizViewStore } from "@stores/quizView/store";
import { useQuizData } from "@utils/hooks/useQuizData"; import { useQuizData } from "@utils/hooks/useQuizData";
import { useRootContainerSize } from "../../contexts/RootContainerWidthContext";
type FooterProps = { type FooterProps = {
setCurrentQuestion: (step: AnyTypedQuizQuestion) => void; stepNumber: number | null;
question: AnyTypedQuizQuestion; nextButton: ReactNode;
setShowContactForm: (show: boolean) => void; prevButton: ReactNode;
setShowResultForm: (show: boolean) => void;
}; };
export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setShowResultForm }: FooterProps) => { export const Footer = ({ stepNumber, nextButton, prevButton }: FooterProps) => {
const theme = useTheme(); const theme = useTheme();
const { questions } = useQuizData();
const { settings, questions } = useQuizData();
const answers = useQuizViewStore(state => state.answers);
const [stepNumber, setStepNumber] = useState(1);
const isMobileMini = useRootContainerSize() < 382;
const isLinear = !questions.some(({ content }) => content.rule.parentId === "root");
const getNextQuestionId = useCallback(() => {
console.log("Смотрим какой вопрос будет дальше. Что у нас сегодня вкусненького? Щя покажу от какого вопроса мы ищем следующий шаг");
console.log(question);
console.log("От вот этого /|");
let readyBeNextQuestion = "";
//вопрос обязателен, анализируем ответ и условия ветвления
if (answers.length) {
const answer = answers.find(({ questionId }) => questionId === question.id);
(question as QuizQuestionBase).content.rule.main.forEach(({ next, rules }) => {
const longerArray = Math.max(
rules[0].answers.length,
answer?.answer && Array.isArray(answer?.answer) ? answer?.answer.length : [answer?.answer].length
);
for (
let i = 0;
i < longerArray;
i++ // Цикл по всем эле­мен­там бОльшего массива
) {
if (Array.isArray(answer?.answer)) {
if (answer?.answer.find((item) => String(item === rules[0].answers[i]))) {
readyBeNextQuestion = next; // Ес­ли хоть один эле­мент от­ли­ча­ет­ся, мас­си­вы не рав­ны
}
return;
}
if (String(rules[0].answers[i]) === answer?.answer) {
readyBeNextQuestion = next; // Ес­ли хоть один эле­мент от­ли­ча­ет­ся, мас­си­вы не рав­ны
}
}
});
if (readyBeNextQuestion) return readyBeNextQuestion;
}
if (!question.required) {//вопрос не обязателен и не нашли совпадений между ответами и условиями ветвления
console.log("вопрос не обязателен ищем дальше");
const defaultQ = question.content.rule.default;
if (defaultQ.length > 1 && defaultQ !== " ") return defaultQ;
//Вопросы типа страница, ползунок, своё поле для ввода и дата не могут иметь больше 1 ребёнка. Пользователь не может настроить там дефолт
//Кинуть на ребёнка надо даже если там нет дефолта
if (
(question?.type === "date" ||
question?.type === "text" ||
question?.type === "number" ||
question?.type === "page") && question.content.rule.children.length === 1
) return question.content.rule.children[0];
}
//ничё не нашли, ищем резулт
console.log("ничё не нашли, ищем резулт ");
return questions.find(q => {
console.log('q.type === "result"', q.type === "result");
console.log('q.content.rule.parentId', q.content.rule.parentId);
console.log('question.content.id', question.content.id);
return q.type === "result" && q.content.rule.parentId === question.content.id;
})?.id;
}, [answers, questions, question]);
const isPreviousButtonDisabled = useMemo(() => {
// Логика для аргумента disabled у кнопки "Назад"
if (isLinear) {
const questionIndex = questions.findIndex(({ id }) => id === question.id);
const previousQuestion = questions[questionIndex - 1];
return previousQuestion ? false : true;
} else {
return question?.content.rule.parentId === "root" ? true : false;
}
}, [questions, isLinear, question?.content.rule.parentId, question.id]);
const isNextButtonDisabled = useMemo(() => {
// Логика для аргумента disabled у кнопки "Далее"
const answer = answers.find(({ questionId }) => questionId === question.id);
if ("required" in question.content && question.content.required && answer) {
return false;
}
if ("required" in question.content && question.content.required && !answer) {
return true;
}
if (isLinear) {
return false;
}
const nextQuestionId = getNextQuestionId();
if (nextQuestionId) {
return false;
} else {
const questionId = question.content.rule.default;
const nextQuestion = questions.find(q => q.id === questionId || q.content.id === questionId) || null;
if (nextQuestion?.type) {
return false;
}
}
}, [answers, getNextQuestionId, isLinear, question.content, question.id]);
const showResult = (nextQuestion: QuizQuestionResult) => {
if (!nextQuestion) return;
const isEmpty = checkEmptyData({ resultData: nextQuestion });
if (nextQuestion) {
if (nextQuestion && settings?.cfg.resultInfo.showResultForm === "before") {
if (isEmpty) {
setShowContactForm(true); //до+пустая = кидать на ФК
} else {
setShowResultForm(true); //до+заполнена = показать
}
}
if (nextQuestion && settings?.cfg.resultInfo.showResultForm === "after") {
if (isEmpty) {
setShowContactForm(true); //после+пустая
} else {
setShowContactForm(true); //после+заполнена = показать ФК
}
}
}
};
const followPreviousStep = () => {
if (isLinear) {
setStepNumber(q => q - 1);
const questionIndex = questions.findIndex(({ id }) => id === question.id);
const previousQuestion = questions[questionIndex - 1];
if (previousQuestion) {
setCurrentQuestion(previousQuestion);
}
return;
}
if (question?.content.rule.parentId !== "root") {
const questionId = question?.content.rule.parentId;
const parent = questions.find(q => q.id === questionId || q.content.id === questionId) || null;
if (parent?.type) {
setCurrentQuestion(parent);
} else {
enqueueSnackbar("не могу получить предыдущий вопрос");
}
} else {
enqueueSnackbar("вы находитесь на первом вопросе");
}
};
const followNextStep = () => {
if (isLinear) {
setStepNumber(q => q + 1);
const questionIndex = questions.findIndex(({ id }) => id === question.id);
const nextQuestion = questions[questionIndex + 1];
if (nextQuestion && nextQuestion.type !== "result") {
setCurrentQuestion(nextQuestion);
} else {
//@ts-ignore
showResult(questions.find(q => q.content.rule.parentId === "line"));
}
return;
}
const nextQuestionId = getNextQuestionId();
if (nextQuestionId) {
const nextQuestion = questions.find(q => q.id === nextQuestionId || q.content.id === nextQuestionId) || null;
if (nextQuestion?.type && nextQuestion.type === "result") {
showResult(nextQuestion);
} else {
//@ts-ignore
setCurrentQuestion(nextQuestion);
}
} else {
enqueueSnackbar("не могу получить последующий вопрос");
}
};
return ( return (
<Box <Box
@ -250,7 +39,7 @@ export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setSh
{/*):(*/} {/*):(*/}
{/* <NameplateLogoFQDark style={{ fontSize: "34px", width:"200px", height:"auto" }} />*/} {/* <NameplateLogoFQDark style={{ fontSize: "34px", width:"200px", height:"auto" }} />*/}
{/*)}*/} {/*)}*/}
{isLinear && {stepNumber !== null &&
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
@ -313,26 +102,8 @@ export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setSh
{questions.length} {questions.length}
</Typography> */} </Typography> */}
</Box> </Box>
<Button {prevButton}
disabled={isPreviousButtonDisabled} {nextButton}
variant="contained"
sx={{ fontSize: "16px", padding: "10px 15px", }}
onClick={followPreviousStep}
>
{isMobileMini ? (
"←"
) : (
"← Назад"
)}
</Button>
<Button
disabled={isNextButtonDisabled}
variant="contained"
sx={{ fontSize: "16px", padding: "10px 15px" }}
onClick={followNextStep}
>
Далее
</Button>
</Box> </Box>
</Box> </Box>
); );

@ -1,9 +1,6 @@
import { Box, useTheme } from "@mui/material"; import { Box, useTheme } from "@mui/material";
import { useEffect, useState } from "react";
import { ContactForm } from "./ContactForm";
import { Footer } from "./Footer"; import { Footer } from "./Footer";
import { ResultForm } from "./ResultForm";
import { Date } from "./questions/Date"; import { Date } from "./questions/Date";
import { Emoji } from "./questions/Emoji"; import { Emoji } from "./questions/Emoji";
import { File } from "./questions/File"; import { File } from "./questions/File";
@ -16,108 +13,67 @@ import { Text } from "./questions/Text";
import { Variant } from "./questions/Variant"; import { Variant } from "./questions/Variant";
import { Varimg } from "./questions/Varimg"; import { Varimg } from "./questions/Varimg";
import type { AnyTypedQuizQuestion } from "../../model/questionTypes/shared"; import type { RealTypedQuizQuestion } from "../../model/questionTypes/shared";
import { NameplateLogoFQ } from "@icons/NameplateLogoFQ"; import { NameplateLogoFQ } from "@icons/NameplateLogoFQ";
import { NameplateLogoFQDark } from "@icons/NameplateLogoFQDark"; import { NameplateLogoFQDark } from "@icons/NameplateLogoFQDark";
import { QuizQuestionResult } from "@model/questionTypes/result";
import { useQuizData } from "@utils/hooks/useQuizData"; import { useQuizData } from "@utils/hooks/useQuizData";
import { notReachable } from "@utils/notReachable"; import { notReachable } from "@utils/notReachable";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import { ReactNode } from "react";
import { useRootContainerSize } from "../../contexts/RootContainerWidthContext"; import { useRootContainerSize } from "../../contexts/RootContainerWidthContext";
export const Question = () => { type Props = {
currentQuestion: RealTypedQuizQuestion;
currentQuestionStepNumber: number | null;
nextButton: ReactNode;
prevButton: ReactNode;
};
export const Question = ({
currentQuestion,
currentQuestionStepNumber,
nextButton,
prevButton,
}: Props) => {
const theme = useTheme(); const theme = useTheme();
const { settings, questions } = useQuizData(); const { settings } = useQuizData();
const [currentQuestion, setCurrentQuestion] = useState<AnyTypedQuizQuestion>();
const [showContactForm, setShowContactForm] = useState<boolean>(false);
const [showResultForm, setShowResultForm] = useState<boolean>(false);
const isMobile = useRootContainerSize() < 650; const isMobile = useRootContainerSize() < 650;
useEffect(() => {
if (settings?.cfg.haveRoot) {//ветвимся
const questionId = settings?.cfg.haveRoot || "";
const nextQuestion = questions.find(q => q.id === questionId || q.content.id === questionId) || null;
if (nextQuestion?.type) {
setCurrentQuestion(nextQuestion);
return;
}
} else {//идём прямо
setCurrentQuestion(questions[0]);
}
}, []);
if (!currentQuestion || currentQuestion.type === "result") return "не смог отобразить вопрос";
return ( return (
<Box <Box sx={{
sx={{ backgroundColor: theme.palette.background.default,
backgroundColor: theme.palette.background.default, height: isMobile ? undefined : "100%"
height: isMobile ? undefined : "100vh" }}>
}} <Box sx={{
> height: "calc(100% - 75px)",
{!showContactForm && !showResultForm && ( width: "100%",
<Box maxWidth: "1440px",
sx={{ padding: "40px 25px 20px",
height: "calc(100vh - 75px)", margin: "0 auto",
width: "100%", overflow: "auto",
maxWidth: "1440px", display: "flex",
padding: "40px 25px 20px", flexDirection: "column",
margin: "0 auto", justifyContent: "space-between"
overflow: "auto", }}>
display: "flex", <QuestionByType question={currentQuestion} />
flexDirection: "column", {quizThemes[settings.cfg.theme].isLight ? (
justifyContent: "space-between" <NameplateLogoFQ style={{ fontSize: "34px", width: "200px", height: "auto" }} />
}} ) : (
> <NameplateLogoFQDark style={{ fontSize: "34px", width: "200px", height: "auto" }} />
<QuestionByType question={currentQuestion} /> )}
{quizThemes[settings.cfg.theme].isLight ? ( </Box>
<NameplateLogoFQ style={{ fontSize: "34px", width: "200px", height: "auto" }} /> <Footer
) : ( stepNumber={currentQuestionStepNumber}
<NameplateLogoFQDark style={{ fontSize: "34px", width: "200px", height: "auto" }} /> prevButton={prevButton}
)} nextButton={nextButton}
</Box> />
)}
{showResultForm && settings?.cfg.resultInfo.showResultForm === "before" && (
<ResultForm
currentQuestion={currentQuestion}
showContactForm={showContactForm}
setShowContactForm={setShowContactForm}
setShowResultForm={setShowResultForm}
/>
)}
{showContactForm && (
<ContactForm
currentQuestion={currentQuestion}
showResultForm={showResultForm}
setShowContactForm={setShowContactForm}
setShowResultForm={setShowResultForm}
/>
)}
{showResultForm && settings?.cfg.resultInfo.showResultForm === "after" && (
<ResultForm
currentQuestion={currentQuestion}
showContactForm={showContactForm}
setShowContactForm={setShowContactForm}
setShowResultForm={setShowResultForm}
/>
)}
{!showContactForm && !showResultForm && (
<Footer
question={currentQuestion}
setCurrentQuestion={setCurrentQuestion}
setShowContactForm={setShowContactForm}
setShowResultForm={setShowResultForm}
/>
)}
</Box> </Box>
); );
}; };
function QuestionByType({ question }: { function QuestionByType({ question }: {
question: Exclude<AnyTypedQuizQuestion, QuizQuestionResult>; question: RealTypedQuizQuestion;
}) { }) {
switch (question.type) { switch (question.type) {
case "variant": return <Variant currentQuestion={question} />; case "variant": return <Variant currentQuestion={question} />;
@ -131,6 +87,6 @@ function QuestionByType({ question }: {
case "file": return <File currentQuestion={question} />; case "file": return <File currentQuestion={question} />;
case "page": return <Page currentQuestion={question} />; case "page": return <Page currentQuestion={question} />;
case "rating": return <Rating currentQuestion={question} />; case "rating": return <Rating currentQuestion={question} />;
default: return notReachable(question); default: notReachable(question);
} }
} }

@ -8,62 +8,21 @@ import {
import { NameplateLogo } from "@icons/NameplateLogo"; import { NameplateLogo } from "@icons/NameplateLogo";
import YoutubeEmbedIframe from "./tools/YoutubeEmbedIframe"; import YoutubeEmbedIframe from "./tools/YoutubeEmbedIframe";
import { setCurrentQuizStep } from "@stores/quizView/store";
import { useQuizData } from "@utils/hooks/useQuizData"; import { useQuizData } from "@utils/hooks/useQuizData";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import { useCallback, useEffect, useMemo } from "react";
import { useRootContainerSize } from "../../contexts/RootContainerWidthContext"; import { useRootContainerSize } from "../../contexts/RootContainerWidthContext";
import type { QuizQuestionResult } from "../../model/questionTypes/result"; import type { QuizQuestionResult } from "../../model/questionTypes/result";
type ResultFormProps = { type ResultFormProps = {
currentQuestion: any; resultQuestion: QuizQuestionResult;
showContactForm: boolean;
setShowContactForm: (show: boolean) => void;
setShowResultForm: (show: boolean) => void;
}; };
export const ResultForm = ({
currentQuestion, export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
showContactForm,
setShowContactForm,
setShowResultForm,
}: ResultFormProps) => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useRootContainerSize() < 650; const isMobile = useRootContainerSize() < 650;
const { settings, questions } = useQuizData(); const { settings } = useQuizData();
const resultQuestion = useMemo(() => {
if (settings?.cfg.haveRoot) {
//ищём для ветвления
return (questions.find(
(question): question is QuizQuestionResult =>
question.type === "result" &&
question.content.rule.parentId === currentQuestion.content.id
) || questions.find(
(question): question is QuizQuestionResult =>
question.type === "result" &&
question.content.rule.parentId === "line"
));
} else {
return questions.find(
(question): question is QuizQuestionResult =>
question.type === "result" &&
question.content.rule.parentId === "line"
);
}
}, [currentQuestion.content.id, questions, settings?.cfg.haveRoot]);
const followNextForm = useCallback(() => {
setShowResultForm(false);
setShowContactForm(true);
}, [setShowContactForm, setShowResultForm]);
useEffect(() => {
if (!resultQuestion) {
followNextForm();
}
}, [followNextForm, resultQuestion]);
if (!resultQuestion) return null;
return ( return (
<Box <Box
@ -72,7 +31,7 @@ export const ResultForm = ({
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
height: "100vh", height: "100%",
width: "100vw", width: "100vw",
pt: "28px", pt: "28px",
overflow: "auto", overflow: "auto",
@ -197,9 +156,9 @@ export const ResultForm = ({
p: "20px", p: "20px",
}} }}
> >
{settings?.cfg.resultInfo.showResultForm === "before" && ( {settings.cfg.resultInfo.showResultForm === "before" && (
<Button <Button
onClick={followNextForm} onClick={() => setCurrentQuizStep("contactform")}
variant="contained" variant="contained"
sx={{ sx={{
p: "10px 20px", p: "10px 20px",
@ -210,7 +169,7 @@ export const ResultForm = ({
{resultQuestion.content.hint.text || "Узнать подробнее"} {resultQuestion.content.hint.text || "Узнать подробнее"}
</Button> </Button>
)} )}
{settings?.cfg.resultInfo.showResultForm === "after" && {settings.cfg.resultInfo.showResultForm === "after" &&
resultQuestion.content.redirect && ( resultQuestion.content.redirect && (
<Button <Button
href={resultQuestion.content.redirect} href={resultQuestion.content.redirect}

@ -8,13 +8,10 @@ import { QuizStartpageAlignType, QuizStartpageType } from "@model/settingsData";
import { useQuizData } from "@utils/hooks/useQuizData"; import { useQuizData } from "@utils/hooks/useQuizData";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import { useRootContainerSize } from "../../contexts/RootContainerWidthContext"; import { useRootContainerSize } from "../../contexts/RootContainerWidthContext";
import { setCurrentQuizStep } from "@stores/quizView/store";
interface Props { export const StartPageViewPublication = () => {
setVisualStartPage: (a: boolean) => void;
}
export const StartPageViewPublication = ({ setVisualStartPage }: Props) => {
const theme = useTheme(); const theme = useTheme();
const { settings } = useQuizData(); const { settings } = useQuizData();
const { isMobileDevice } = useUADevice(); const { isMobileDevice } = useUADevice();
@ -52,9 +49,7 @@ export const StartPageViewPublication = ({ setVisualStartPage }: Props) => {
height: height:
settings.cfg.startpageType === "centered" settings.cfg.startpageType === "centered"
? "275px" ? "275px"
: settings.cfg.startpageType === "expanded" : "100%",
? "100vh"
: "100%",
borderRadius: settings.cfg.startpageType === "centered" ? "10px" : "0", borderRadius: settings.cfg.startpageType === "centered" ? "10px" : "0",
overflow: "hidden", overflow: "hidden",
"& iframe": { "& iframe": {
@ -76,8 +71,8 @@ export const StartPageViewPublication = ({ setVisualStartPage }: Props) => {
<Paper <Paper
className="settings-preview-draghandle" className="settings-preview-draghandle"
sx={{ sx={{
height: "100vh", height: "100%",
width: "100vw", width: "100%",
background: background:
settings.cfg.startpageType === "expanded" && !isMobile settings.cfg.startpageType === "expanded" && !isMobile
? settings.cfg.startpage.position === "left" ? settings.cfg.startpage.position === "left"
@ -180,7 +175,7 @@ export const StartPageViewPublication = ({ setVisualStartPage }: Props) => {
padding: "10px 15px", padding: "10px 15px",
width: settings.cfg.startpageType === "standard" ? "100%" : "auto", width: settings.cfg.startpageType === "standard" ? "100%" : "auto",
}} }}
onClick={() => setVisualStartPage(false)} onClick={() => setCurrentQuizStep("question")}
> >
{settings.cfg.startpage.button.trim() ? settings.cfg.startpage.button : "Пройти тест"} {settings.cfg.startpage.button.trim() ? settings.cfg.startpage.button : "Пройти тест"}
</Button> </Button>
@ -277,7 +272,7 @@ function QuizPreviewLayoutByType({
flexDirection: "column-reverse", flexDirection: "column-reverse",
flexGrow: 1, flexGrow: 1,
justifyContent: "flex-end", justifyContent: "flex-end",
height: "100vh", height: "100%",
"&::-webkit-scrollbar": { width: 0 }, "&::-webkit-scrollbar": { width: 0 },
}} }}
> >
@ -333,7 +328,7 @@ function QuizPreviewLayoutByType({
flexDirection: alignType === "left" ? (isMobile ? "column-reverse" : "row") : "row-reverse", flexDirection: alignType === "left" ? (isMobile ? "column-reverse" : "row") : "row-reverse",
flexGrow: 1, flexGrow: 1,
justifyContent: isMobile ? "flex-end" : undefined, justifyContent: isMobile ? "flex-end" : undefined,
height: "100vh", height: "100%",
"&::-webkit-scrollbar": { width: 0 }, "&::-webkit-scrollbar": { width: 0 },
}} }}
> >

@ -1,43 +1,99 @@
import { Box, ThemeProvider } from "@mui/material"; import { Button, ThemeProvider } from "@mui/material";
import { useQuizViewStore } from "@stores/quizView/store";
import { useQuestionFlowControl } from "@utils/hooks/useQuestionFlowControl";
import { useQuizData } from "@utils/hooks/useQuizData"; import { useQuizData } from "@utils/hooks/useQuizData";
import { notReachable } from "@utils/notReachable";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import { useEffect, useState } from "react"; import { ReactElement, useEffect } from "react";
import { ApologyPage } from "./ApologyPage"; import { useRootContainerSize } from "../../contexts/RootContainerWidthContext";
import { ContactForm } from "./ContactForm";
import { Question } from "./Question"; import { Question } from "./Question";
import { ResultForm } from "./ResultForm";
import { StartPageViewPublication } from "./StartPageViewPublication"; import { StartPageViewPublication } from "./StartPageViewPublication";
export const ViewPage = () => { export default function ViewPublicationPage() {
const { settings, questions, recentlyCompleted } = useQuizData(); const { settings, recentlyCompleted } = useQuizData();
const [visualStartPage, setVisualStartPage] = useState<boolean>(); let currentQuizStep = useQuizViewStore(state => state.currentQuizStep);
const isMobileMini = useRootContainerSize() < 382;
const {
currentQuestion,
currentQuestionStepNumber,
isNextButtonDisabled,
isPreviousButtonDisabled,
moveToPrevQuestion,
moveToNextQuestion,
showResultAfterContactForm,
} = useQuestionFlowControl();
useEffect(() => { useEffect(function setFaviconAndTitle() {
const link = document.querySelector('link[rel="icon"]'); const link = document.querySelector('link[rel="icon"]');
if (link && settings.cfg.startpage.favIcon) { if (link && settings.cfg.startpage.favIcon) {
link.setAttribute("href", settings?.cfg.startpage.favIcon); link.setAttribute("href", settings.cfg.startpage.favIcon);
} }
document.title = settings.name; document.title = settings.name;
setVisualStartPage(!settings.cfg.noStartPage);
}, [settings]); }, [settings]);
const questionsCount = questions.filter(({ type }) => type !== null && type !== "result").length; let quizStepElement: ReactElement;
if (questionsCount === 0) return <ApologyPage message="Нет созданных вопросов" />;
if (recentlyCompleted) throw new Error("Quiz already completed");
if (currentQuizStep === "startpage" && settings.cfg.noStartPage) currentQuizStep = "question";
switch (currentQuizStep) {
case "startpage": {
quizStepElement = <StartPageViewPublication />;
break;
}
case "question": {
if (currentQuestion.type === "result") {
quizStepElement = <ResultForm resultQuestion={currentQuestion} />;
break;
}
quizStepElement = (
<Question
currentQuestion={currentQuestion}
currentQuestionStepNumber={currentQuestionStepNumber}
prevButton={
<Button
disabled={isPreviousButtonDisabled}
variant="contained"
sx={{ fontSize: "16px", padding: "10px 15px" }}
onClick={moveToPrevQuestion}
>
{isMobileMini ? "←" : "← Назад"}
</Button>
}
nextButton={
<Button
disabled={isNextButtonDisabled}
variant="contained"
sx={{ fontSize: "16px", padding: "10px 15px" }}
onClick={moveToNextQuestion}
>
Далее
</Button>
}
/>
);
break;
}
case "contactform": {
quizStepElement = (
<ContactForm
currentQuestion={currentQuestion}
onShowResult={showResultAfterContactForm}
/>
);
break;
}
default: notReachable(currentQuizStep);
}
return ( return (
<ThemeProvider theme={quizThemes[settings.cfg.theme || "StandardTheme"]}> <ThemeProvider theme={quizThemes[settings.cfg.theme || "StandardTheme"].theme}>
{recentlyCompleted ? ( {quizStepElement}
<ApologyPage message="Вы уже прошли этот опрос" />
) : (
<Box>
{visualStartPage ? (
<StartPageViewPublication setVisualStartPage={setVisualStartPage} />
) : (
<Question />
)}
</Box>
)}
</ThemeProvider> </ThemeProvider>
); );
}; }

@ -11,6 +11,7 @@ import { sendAnswer } from "@api/quizRelase";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import { useQuizData } from "@utils/hooks/useQuizData"; import { useQuizData } from "@utils/hooks/useQuizData";
import { useQuizId } from "../../../contexts/QuizIdContext";
type DateProps = { type DateProps = {
currentQuestion: QuizQuestionDate; currentQuestion: QuizQuestionDate;
@ -18,7 +19,7 @@ type DateProps = {
export const Date = ({ currentQuestion }: DateProps) => { export const Date = ({ currentQuestion }: DateProps) => {
const theme = useTheme(); const theme = useTheme();
const qid = useQuizId();
const { settings } = useQuizData(); const { settings } = useQuizData();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const answer = answers.find( const answer = answers.find(
@ -61,7 +62,7 @@ export const Date = ({ currentQuestion }: DateProps) => {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: moment(date).format("YYYY.MM.DD"), body: moment(date).format("YYYY.MM.DD"),
qid: settings.qid, qid,
}); });
updateAnswer( updateAnswer(

@ -16,8 +16,8 @@ import RadioIcon from "@ui_kit/RadioIcon";
import { sendAnswer } from "@api/quizRelase"; import { sendAnswer } from "@api/quizRelase";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useQuizId } from "../../../contexts/QuizIdContext";
import type { QuizQuestionEmoji } from "../../../model/questionTypes/emoji"; import type { QuizQuestionEmoji } from "../../../model/questionTypes/emoji";
import { useQuizData } from "@utils/hooks/useQuizData";
type EmojiProps = { type EmojiProps = {
currentQuestion: QuizQuestionEmoji; currentQuestion: QuizQuestionEmoji;
@ -25,8 +25,7 @@ type EmojiProps = {
export const Emoji = ({ currentQuestion }: EmojiProps) => { export const Emoji = ({ currentQuestion }: EmojiProps) => {
const theme = useTheme(); const theme = useTheme();
const qid = useQuizId();
const { settings } = useQuizData();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const { answer } = const { answer } =
answers.find( answers.find(
@ -110,7 +109,7 @@ export const Emoji = ({ currentQuestion }: EmojiProps) => {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: currentQuestion.content.variants[index].extendedText + " " + currentQuestion.content.variants[index].answer, body: currentQuestion.content.variants[index].extendedText + " " + currentQuestion.content.variants[index].answer,
qid: settings.qid qid,
}); });
updateAnswer( updateAnswer(
@ -131,7 +130,7 @@ export const Emoji = ({ currentQuestion }: EmojiProps) => {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: "", body: "",
qid: settings.qid qid,
}); });
} catch (e) { } catch (e) {

@ -14,10 +14,10 @@ import UploadIcon from "@icons/UploadIcon";
import { sendAnswer, sendFile } from "@api/quizRelase"; import { sendAnswer, sendFile } from "@api/quizRelase";
import Info from "@icons/Info"; import Info from "@icons/Info";
import type { UploadFileType } from "@model/questionTypes/file"; import type { UploadFileType } from "@model/questionTypes/file";
import { useQuizData } from "@utils/hooks/useQuizData";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import type { DragEvent } from "react"; import type { DragEvent } from "react";
import { useState, type ChangeEvent } from "react"; import { useState, type ChangeEvent } from "react";
import { useQuizId } from "../../../contexts/QuizIdContext";
import { useRootContainerSize } from "../../../contexts/RootContainerWidthContext"; import { useRootContainerSize } from "../../../contexts/RootContainerWidthContext";
import type { QuizQuestionFile } from "../../../model/questionTypes/file"; import type { QuizQuestionFile } from "../../../model/questionTypes/file";
@ -117,9 +117,8 @@ const UPLOAD_FILE_DESCRIPTIONS_MAP: Record<
export const File = ({ currentQuestion }: FileProps) => { export const File = ({ currentQuestion }: FileProps) => {
const theme = useTheme(); const theme = useTheme();
const { settings } = useQuizData();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const qid = useQuizId();
const [statusModal, setStatusModal] = useState<"errorType" | "errorSize" | "picture" | "video" | "audio" | "document" | "">(""); const [statusModal, setStatusModal] = useState<"errorType" | "errorSize" | "picture" | "video" | "audio" | "document" | "">("");
const answer = answers.find( const answer = answers.find(
@ -148,16 +147,14 @@ export const File = ({ currentQuestion }: FileProps) => {
file: file, file: file,
name: file.name name: file.name
}, },
qid: settings.qid qid,
}); });
console.log(data); console.log(data);
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
//@ts-ignore body: `https://storage.yandexcloud.net/squizanswer/${qid}/${currentQuestion.id}/${data.data.fileIDMap[currentQuestion.id]}`,
body: `https://storage.yandexcloud.net/squizanswer/${settings.qid}/${currentQuestion.id}/${data.data.fileIDMap[currentQuestion.id]}`, qid,
//@ts-ignore
qid: settings.qid
}); });
updateAnswer( updateAnswer(

@ -12,8 +12,8 @@ import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon"; import RadioIcon from "@ui_kit/RadioIcon";
import { sendAnswer } from "@api/quizRelase"; import { sendAnswer } from "@api/quizRelase";
import { useQuizData } from "@utils/hooks/useQuizData";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useQuizId } from "../../../contexts/QuizIdContext";
import { useRootContainerSize } from "../../../contexts/RootContainerWidthContext"; import { useRootContainerSize } from "../../../contexts/RootContainerWidthContext";
import type { QuizQuestionImages } from "../../../model/questionTypes/images"; import type { QuizQuestionImages } from "../../../model/questionTypes/images";
@ -22,7 +22,7 @@ type ImagesProps = {
}; };
export const Images = ({ currentQuestion }: ImagesProps) => { export const Images = ({ currentQuestion }: ImagesProps) => {
const { settings } = useQuizData(); const qid = useQuizId();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const theme = useTheme(); const theme = useTheme();
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer; const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer;
@ -74,7 +74,7 @@ export const Images = ({ currentQuestion }: ImagesProps) => {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: `${currentQuestion.content.variants[index].answer} <img style="width:100%; max-width:250px; max-height:250px" src="${currentQuestion.content.variants[index].extendedText}"/>`, body: `${currentQuestion.content.variants[index].answer} <img style="width:100%; max-width:250px; max-height:250px" src="${currentQuestion.content.variants[index].extendedText}"/>`,
qid: settings.qid qid,
}); });
updateAnswer( updateAnswer(
@ -94,7 +94,7 @@ export const Images = ({ currentQuestion }: ImagesProps) => {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: "", body: "",
qid: settings.qid qid,
}); });
} catch (e) { } catch (e) {

@ -13,6 +13,7 @@ import type { QuizQuestionNumber } from "../../../model/questionTypes/number";
import { useQuizData } from "@utils/hooks/useQuizData"; import { useQuizData } from "@utils/hooks/useQuizData";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import { useQuizId } from "../../../contexts/QuizIdContext";
import { useRootContainerSize } from "../../../contexts/RootContainerWidthContext"; import { useRootContainerSize } from "../../../contexts/RootContainerWidthContext";
type NumberProps = { type NumberProps = {
@ -20,6 +21,7 @@ type NumberProps = {
}; };
export const Number = ({ currentQuestion }: NumberProps) => { export const Number = ({ currentQuestion }: NumberProps) => {
const qid = useQuizId();
const { settings } = useQuizData(); const { settings } = useQuizData();
const [inputValue, setInputValue] = useState<string>("0"); const [inputValue, setInputValue] = useState<string>("0");
const [minRange, setMinRange] = useState<string>("0"); const [minRange, setMinRange] = useState<string>("0");
@ -36,8 +38,7 @@ export const Number = ({ currentQuestion }: NumberProps) => {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: value, body: value,
//@ts-ignore qid,
qid: settings.qid,
}); });
updateAnswer(currentQuestion.id, value); updateAnswer(currentQuestion.id, value);

@ -1,7 +1,5 @@
import { Box, Typography, useTheme } from "@mui/material"; import { Box, Typography, useTheme } from "@mui/material";
import { useQuizViewStore, updateAnswer } from "@stores/quizView/store";
import type { QuizQuestionPage } from "../../../model/questionTypes/page"; import type { QuizQuestionPage } from "../../../model/questionTypes/page";
import YoutubeEmbedIframe from "../tools/YoutubeEmbedIframe"; import YoutubeEmbedIframe from "../tools/YoutubeEmbedIframe";
@ -11,8 +9,6 @@ type PageProps = {
export const Page = ({ currentQuestion }: PageProps) => { export const Page = ({ currentQuestion }: PageProps) => {
const theme = useTheme(); const theme = useTheme();
const { answers } = useQuizViewStore();
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
return ( return (
<Box> <Box>
@ -46,7 +42,7 @@ export const Page = ({ currentQuestion }: PageProps) => {
<YoutubeEmbedIframe <YoutubeEmbedIframe
containerSX={{ containerSX={{
width: "100%", width: "100%",
height: "calc( 100vh - 270px)", height: "calc(100% - 270px)",
maxHeight: "80vh", maxHeight: "80vh",
objectFit: "contain", objectFit: "contain",
}} }}

@ -16,8 +16,8 @@ import LikeIcon from "@icons/questionsPage/likeIcon";
import TropfyIcon from "@icons/questionsPage/tropfyIcon"; import TropfyIcon from "@icons/questionsPage/tropfyIcon";
import { sendAnswer } from "@api/quizRelase"; import { sendAnswer } from "@api/quizRelase";
import { useQuizData } from "@utils/hooks/useQuizData";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useQuizId } from "../../../contexts/QuizIdContext";
import { useRootContainerSize } from "../../../contexts/RootContainerWidthContext"; import { useRootContainerSize } from "../../../contexts/RootContainerWidthContext";
import type { QuizQuestionRating } from "../../../model/questionTypes/rating"; import type { QuizQuestionRating } from "../../../model/questionTypes/rating";
@ -57,7 +57,7 @@ const buttonRatingForm = [
]; ];
export const Rating = ({ currentQuestion }: RatingProps) => { export const Rating = ({ currentQuestion }: RatingProps) => {
const { settings } = useQuizData(); const qid = useQuizId();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const theme = useTheme(); const theme = useTheme();
const isMobile = useRootContainerSize() < 650; const isMobile = useRootContainerSize() < 650;
@ -98,7 +98,7 @@ export const Rating = ({ currentQuestion }: RatingProps) => {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: String(value) + " из " + currentQuestion.content.steps, body: String(value) + " из " + currentQuestion.content.steps,
qid: settings.qid qid,
}); });
updateAnswer(currentQuestion.id, String(value)); updateAnswer(currentQuestion.id, String(value));

@ -2,12 +2,12 @@ import { Box, Typography, useTheme } from "@mui/material";
import { Select as SelectComponent } from "../tools//Select"; import { Select as SelectComponent } from "../tools//Select";
import { useQuizViewStore, updateAnswer, deleteAnswer } from "@stores/quizView/store"; import { deleteAnswer, updateAnswer, useQuizViewStore } from "@stores/quizView/store";
import type { QuizQuestionSelect } from "../../../model/questionTypes/select";
import { enqueueSnackbar } from "notistack";
import { sendAnswer } from "@api/quizRelase"; import { sendAnswer } from "@api/quizRelase";
import { useQuizData } from "@utils/hooks/useQuizData"; import { enqueueSnackbar } from "notistack";
import { useQuizId } from "../../../contexts/QuizIdContext";
import type { QuizQuestionSelect } from "../../../model/questionTypes/select";
type SelectProps = { type SelectProps = {
currentQuestion: QuizQuestionSelect; currentQuestion: QuizQuestionSelect;
@ -15,8 +15,7 @@ type SelectProps = {
export const Select = ({ currentQuestion }: SelectProps) => { export const Select = ({ currentQuestion }: SelectProps) => {
const theme = useTheme(); const theme = useTheme();
const qid = useQuizId();
const { settings } = useQuizData();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const { answer } = const { answer } =
answers.find( answers.find(
@ -47,7 +46,7 @@ export const Select = ({ currentQuestion }: SelectProps) => {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: "", body: "",
qid: settings.qid qid,
}); });
} catch (e) { } catch (e) {
@ -61,7 +60,7 @@ export const Select = ({ currentQuestion }: SelectProps) => {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: String(currentQuestion.content.variants[Number(value)].answer), body: String(currentQuestion.content.variants[Number(value)].answer),
qid: settings.qid qid,
}); });
updateAnswer(currentQuestion.id, String(value)); updateAnswer(currentQuestion.id, String(value));

@ -2,13 +2,13 @@ import { Box, Typography, useTheme } from "@mui/material";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useQuizViewStore, updateAnswer } from "@stores/quizView/store"; import { updateAnswer, useQuizViewStore } from "@stores/quizView/store";
import type { QuizQuestionText } from "../../../model/questionTypes/text";
import { enqueueSnackbar } from "notistack";
import { sendAnswer } from "@api/quizRelase"; import { sendAnswer } from "@api/quizRelase";
import { enqueueSnackbar } from "notistack";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import { useQuizData } from "@utils/hooks/useQuizData"; import { useQuizId } from "../../../contexts/QuizIdContext";
import type { QuizQuestionText } from "../../../model/questionTypes/text";
type TextProps = { type TextProps = {
currentQuestion: QuizQuestionText; currentQuestion: QuizQuestionText;
@ -16,7 +16,7 @@ type TextProps = {
export const Text = ({ currentQuestion }: TextProps) => { export const Text = ({ currentQuestion }: TextProps) => {
const theme = useTheme(); const theme = useTheme();
const { settings } = useQuizData(); const qid = useQuizId();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {}; const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
@ -26,7 +26,7 @@ export const Text = ({ currentQuestion }: TextProps) => {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: text, body: text,
qid: settings.qid qid,
}); });

@ -30,6 +30,7 @@ import { enqueueSnackbar } from "notistack";
import type { QuestionVariant } from "../../../model/questionTypes/shared"; import type { QuestionVariant } from "../../../model/questionTypes/shared";
import type { QuizQuestionVariant } from "../../../model/questionTypes/variant"; import type { QuizQuestionVariant } from "../../../model/questionTypes/variant";
import { useQuizData } from "@utils/hooks/useQuizData"; import { useQuizData } from "@utils/hooks/useQuizData";
import { useQuizId } from "../../../contexts/QuizIdContext";
const TextField = MuiTextField as unknown as FC<TextFieldProps>; const TextField = MuiTextField as unknown as FC<TextFieldProps>;
@ -135,8 +136,9 @@ const VariantItem = ({
index, index,
own = false, own = false,
}: VariantItemProps) => { }: VariantItemProps) => {
const { settings } = useQuizData();
const theme = useTheme(); const theme = useTheme();
const { settings } = useQuizData();
const qid = useQuizId();
return ( return (
<FormControlLabel <FormControlLabel
@ -186,13 +188,12 @@ const VariantItem = ({
const currentAnswer = typeof answer !== "string" ? answer || [] : []; const currentAnswer = typeof answer !== "string" ? answer || [] : [];
try { try {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: currentAnswer.includes(variantId) body: currentAnswer.includes(variantId)
? currentAnswer?.filter((item) => item !== variantId) ? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId], : [...currentAnswer, variantId],
qid: settings.qid qid,
}); });
updateAnswer( updateAnswer(
@ -201,21 +202,18 @@ const VariantItem = ({
? currentAnswer?.filter((item) => item !== variantId) ? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId] : [...currentAnswer, variantId]
); );
} catch (e) { } catch (e) {
enqueueSnackbar("ответ не был засчитан"); enqueueSnackbar("ответ не был засчитан");
} }
return; return;
} }
try { try {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: currentQuestion.content.variants[index].answer, body: currentQuestion.content.variants[index].answer,
qid: settings.qid qid,
}); });
updateAnswer(currentQuestion.id, variantId); updateAnswer(currentQuestion.id, variantId);
@ -230,7 +228,7 @@ const VariantItem = ({
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: "", body: "",
qid: settings.qid qid,
}); });
} catch (e) { } catch (e) {
@ -239,7 +237,6 @@ const VariantItem = ({
deleteAnswer(currentQuestion.id); deleteAnswer(currentQuestion.id);
} }
}} }}
/> />
); );
}; };

@ -16,6 +16,7 @@ import BlankImage from "@icons/BlankImage";
import { useQuizData } from "@utils/hooks/useQuizData"; import { useQuizData } from "@utils/hooks/useQuizData";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useQuizId } from "../../../contexts/QuizIdContext";
import { useRootContainerSize } from "../../../contexts/RootContainerWidthContext"; import { useRootContainerSize } from "../../../contexts/RootContainerWidthContext";
import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg"; import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg";
@ -26,6 +27,7 @@ type VarimgProps = {
export const Varimg = ({ currentQuestion }: VarimgProps) => { export const Varimg = ({ currentQuestion }: VarimgProps) => {
const { settings } = useQuizData(); const { settings } = useQuizData();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const qid = useQuizId();
const theme = useTheme(); const theme = useTheme();
const isMobile = useRootContainerSize() < 650; const isMobile = useRootContainerSize() < 650;
@ -89,7 +91,7 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: `${currentQuestion.content.variants[index].answer} <img style="width:100%; max-width:250px; max-height:250px" src="${currentQuestion.content.variants[index].extendedText}"/>`, body: `${currentQuestion.content.variants[index].answer} <img style="width:100%; max-width:250px; max-height:250px" src="${currentQuestion.content.variants[index].extendedText}"/>`,
qid: settings.qid qid,
}); });
updateAnswer( updateAnswer(
@ -108,7 +110,7 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: "", body: "",
qid: settings.qid qid,
}); });
} catch (e) { } catch (e) {

@ -1,17 +1,16 @@
import { QuizQuestionResult } from "@model/questionTypes/result" import { QuizQuestionResult } from "@model/questionTypes/result";
export const checkEmptyData = ({ resultData }: { resultData: QuizQuestionResult }) => { export const isResultQuestionEmpty = (resultQuestion: QuizQuestionResult) => {
let check = true
if ( if (
(resultData.title.length > 0 && resultData.title !== " ") || (resultQuestion.title.length > 0 && resultQuestion.title !== " ")
(resultData.description.length > 0 && resultData.description !== " ") || || (resultQuestion.description.length > 0 && resultQuestion.description !== " ")
(resultData.content.back.length > 0 && resultData.content.back !== " ") || || (resultQuestion.content.back.length > 0 && resultQuestion.content.back !== " ")
(resultData.content.originalBack.length > 0 && resultData.content.originalBack !== " ") || || (resultQuestion.content.originalBack.length > 0 && resultQuestion.content.originalBack !== " ")
(resultData.content.innerName.length > 0 && resultData.content.innerName !== " ") || || (resultQuestion.content.innerName.length > 0 && resultQuestion.content.innerName !== " ")
(resultData.content.text.length > 0 && resultData.content.text !== " ") || || (resultQuestion.content.text.length > 0 && resultQuestion.content.text !== " ")
(resultData.content.video.length > 0 && resultData.content.video !== " ") || || (resultQuestion.content.video.length > 0 && resultQuestion.content.video !== " ")
(resultData.content.hint.text.length > 0 && resultData.content.hint.text !== " " ) || (resultQuestion.content.hint.text.length > 0 && resultQuestion.content.hint.text !== " ")
) check = false ) return false;
return check
} return true;
};

@ -4,6 +4,7 @@ import { nanoid } from "nanoid";
import { create } from "zustand"; import { create } from "zustand";
import { devtools } from "zustand/middleware"; import { devtools } from "zustand/middleware";
import type { Moment } from "moment"; import type { Moment } from "moment";
import { QuizStep } from "@model/settingsData";
type Answer = { type Answer = {
questionId: string; questionId: string;
@ -18,6 +19,7 @@ type OwnVariant = {
interface QuizViewStore { interface QuizViewStore {
answers: Answer[]; answers: Answer[];
ownVariants: OwnVariant[]; ownVariants: OwnVariant[];
currentQuizStep: QuizStep;
} }
export const useQuizViewStore = create<QuizViewStore>()( export const useQuizViewStore = create<QuizViewStore>()(
@ -25,6 +27,7 @@ export const useQuizViewStore = create<QuizViewStore>()(
(set, get) => ({ (set, get) => ({
answers: [], answers: [],
ownVariants: [], ownVariants: [],
currentQuizStep: "startpage",
}), }),
{ {
name: "quizView", name: "quizView",
@ -42,8 +45,8 @@ function setProducedState<A extends string | { type: string; }>(
} }
export const updateAnswer = ( export const updateAnswer = (
questionId: string, questionId: string,
answer: string | string[] | Moment answer: string | string[] | Moment
) => setProducedState(state => { ) => setProducedState(state => {
const index = state.answers.findIndex(answer => questionId === answer.questionId); const index = state.answers.findIndex(answer => questionId === answer.questionId);
@ -93,4 +96,11 @@ export const deleteOwnVariant = (id: string) => useQuizViewStore.setState(state
}), false, { }), false, {
type: "deleteOwnVariant", type: "deleteOwnVariant",
id id
}); });
export const setCurrentQuizStep = (currentQuizStep: QuizStep) => useQuizViewStore.setState({
currentQuizStep
}, false, {
type: "setCurrentQuizStep",
currentQuizStep
});

@ -0,0 +1,27 @@
import { Box, Typography } from "@mui/material";
import { FallbackProps } from "react-error-boundary";
export default function ErrorBoundaryFallback({ error }: FallbackProps) {
let message = "Что-то пошло не так";
if (error.message === "No questions found") message = "Нет созданных вопросов";
if (error.message === "Quiz already completed") message = "Вы уже прошли этот опрос";
return (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100%",
}}
>
<Typography
sx={{
textAlign: "center",
}}
>{message}</Typography>
</Box>
);
}

@ -0,0 +1,213 @@
import { QuizQuestionResult } from "@model/questionTypes/result";
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { setCurrentQuizStep, useQuizViewStore } from "@stores/quizView/store";
import { useCallback, useDebugValue, useMemo, useState } from "react";
import { isResultQuestionEmpty } from "../../pages/ViewPublicationPage/tools/checkEmptyData";
import { useQuizData } from "./useQuizData";
import { devlog } from "@frontend/kitui";
export function useQuestionFlowControl() {
const { settings, questions } = useQuizData();
const isLinear = questions.every(({ content }) => content.rule.parentId !== "root");
const [currentQuestion, setCurrentQuestion] = useState<AnyTypedQuizQuestion>(getFirstQuestion);
const answers = useQuizViewStore(state => state.answers);
const questionIndex = isLinear ? questions.indexOf(currentQuestion) : null;
const currentQuestionStepNumber = questionIndex && questionIndex + 1;
function getFirstQuestion() {
if (questions.length === 0) throw new Error("No questions found");
if (settings.cfg.haveRoot) {
const nextQuestion = questions.find(
question => question.id === settings.cfg.haveRoot || question.content.id === settings.cfg.haveRoot
);
if (!nextQuestion) throw new Error("Root question not found");
return nextQuestion;
}
return questions[0];
}
const nextQuestionId = useMemo(() => {
console.log("Смотрим какой вопрос будет дальше. Что у нас сегодня вкусненького? Щя покажу от какого вопроса мы ищем следующий шаг");
console.log(currentQuestion);
console.log("От вот этого /|");
let readyBeNextQuestion = "";
//вопрос обязателен, анализируем ответ и условия ветвления
if (answers.length) {
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id);
currentQuestion.content.rule.main.forEach(({ next, rules }) => {
const longerArray = Math.max(
rules[0].answers.length,
answer?.answer && Array.isArray(answer?.answer) ? answer?.answer.length : [answer?.answer].length
);
for (let i = 0; i < longerArray; i++) {
if (Array.isArray(answer?.answer)) {
if (answer?.answer.find((item) => String(item === rules[0].answers[i]))) {
readyBeNextQuestion = next; // Ес­ли хоть один эле­мент от­ли­ча­ет­ся, мас­си­вы не рав­ны
}
return;
}
if (String(rules[0].answers[i]) === answer?.answer) {
readyBeNextQuestion = next; // Ес­ли хоть один эле­мент от­ли­ча­ет­ся, мас­си­вы не рав­ны
}
}
});
if (readyBeNextQuestion) return readyBeNextQuestion;
}
if (!currentQuestion.required) {//вопрос не обязателен и не нашли совпадений между ответами и условиями ветвления
console.log("вопрос не обязателен ищем дальше");
const defaultNextQuestion = currentQuestion.content.rule.default;
if (defaultNextQuestion.length > 1 && defaultNextQuestion !== " ") return defaultNextQuestion;
//Вопросы типа страница, ползунок, своё поле для ввода и дата не могут иметь больше 1 ребёнка. Пользователь не может настроить там дефолт
//Кинуть на ребёнка надо даже если там нет дефолта
if (
["date", "page", "text", "number"].includes(currentQuestion.type)
&& currentQuestion.content.rule.children.length === 1
) return currentQuestion.content.rule.children[0];
}
//ничё не нашли, ищем резулт
console.log("ничё не нашли, ищем резулт ");
return questions.find(q => {
return q.type === "result" && q.content.rule.parentId === currentQuestion.content.id;
})?.id;
}, [answers, currentQuestion, questions]);
const nextQuestion = questions.find(q => q.id === nextQuestionId || q.content.id === nextQuestionId);
const resultQuestion = useMemo(() => {
if (currentQuestion.type === "result") return currentQuestion;
if (settings.cfg.haveRoot) return questions.find((question): question is QuizQuestionResult => {
return question.type === "result" && question.content.rule.parentId === currentQuestion.content.id;
});
return questions.find((question): question is QuizQuestionResult => {
return question.type === "result" && question.content.rule.parentId === "line";
});
}, [currentQuestion, questions, settings.cfg.haveRoot]);
const showResult = useCallback((resultQuestion: QuizQuestionResult) => {
if (settings.cfg.resultInfo.showResultForm === "before" && !isResultQuestionEmpty(resultQuestion)) {
return setCurrentQuestion(resultQuestion);
}
setCurrentQuizStep("contactform");
}, [settings.cfg.resultInfo.showResultForm]);
const showResultAfterContactForm = useCallback(() => {
if (settings.cfg.resultInfo.showResultForm === "before") return;
if (!resultQuestion) throw new Error("Result question not found");
if (isResultQuestionEmpty(resultQuestion)) {
devlog("Result question is empty");
return;
}
setCurrentQuizStep("question");
setCurrentQuestion(resultQuestion);
}, [resultQuestion, settings.cfg.resultInfo.showResultForm]);
const moveToPrevQuestionLinear = useCallback(() => {
if (questionIndex === null) return;
const question = questions[questionIndex - 1];
if (question && question.type !== "result") {
setCurrentQuestion(question);
}
}, [questionIndex, questions]);
const moveToNextQuestionLinear = useCallback(() => {
if (questionIndex === null) return;
const question = questions[questionIndex + 1];
if (question && question.type !== "result") {
return setCurrentQuestion(question);
}
if (!resultQuestion) throw new Error("Result question not found");
showResult(resultQuestion);
}, [questionIndex, questions, resultQuestion, showResult]);
const moveToPrevQuestionBranching = useCallback(() => {
if (currentQuestion.content.rule.parentId === "root") throw new Error("No question to go back to");
const questionId = currentQuestion.content.rule.parentId;
const parent = questions.find(q => q.id === questionId || q.content.id === questionId) || null;
if (!parent || parent.type === "result") throw new Error("Parent question not found");
setCurrentQuestion(parent);
}, [currentQuestion.content.rule.parentId, questions]);
const moveToNextQuestionBranching = useCallback(() => {
if (!nextQuestion) throw new Error("Next question not found");
if (nextQuestion.type === "result") {
showResult(nextQuestion);
} else {
setCurrentQuestion(nextQuestion);
}
}, [nextQuestion, showResult]);
const isPreviousButtonDisabled = useMemo(() => {
if (isLinear) {
const questionIndex = questions.findIndex(({ id }) => id === currentQuestion.id);
const previousQuestion = questions[questionIndex - 1];
return previousQuestion ? false : true;
} else {
return currentQuestion.content.rule.parentId === "root" ? true : false;
}
}, [questions, isLinear, currentQuestion.content.rule.parentId, currentQuestion.id]);
const isNextButtonDisabled = useMemo(() => {
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id);
if ("required" in currentQuestion.content && currentQuestion.content.required) {
return !answer;
}
if (isLinear) return false;
if (nextQuestion) {
return false;
} else {
const questionId = currentQuestion.content.rule.default;
const nextQuestion = questions.find(q => q.id === questionId || q.content.id === questionId) || null;
if (nextQuestion?.type) return false;
}
return true;
}, [answers, currentQuestion.content, currentQuestion.id, isLinear, nextQuestion, questions]);
useDebugValue({
isLinear,
currentQuestion,
nextQuestion: questions.find(q => q.content.id === nextQuestionId),
resultQuestion,
});
return {
currentQuestion,
currentQuestionStepNumber,
isNextButtonDisabled,
isPreviousButtonDisabled,
moveToPrevQuestion: isLinear ? moveToPrevQuestionLinear : moveToPrevQuestionBranching,
moveToNextQuestion: isLinear ? moveToNextQuestionLinear : moveToNextQuestionBranching,
showResultAfterContactForm,
};
}

@ -1,7 +1,6 @@
import { getData } from "@api/quizRelase"; import { getData } from "@api/quizRelase";
import { parseQuizData } from "@model/api/getQuizData"; import { parseQuizData } from "@model/api/getQuizData";
import { QuizSettings } from "@model/settingsData"; import { QuizSettings } from "@model/settingsData";
import { enqueueSnackbar } from "notistack";
import useSWR from "swr"; import useSWR from "swr";
import { useQuizId } from "../../contexts/QuizIdContext"; import { useQuizId } from "../../contexts/QuizIdContext";
import { replaceSpacesToEmptyLines } from "../../pages/ViewPublicationPage/tools/replaceSpacesToEmptyLines"; import { replaceSpacesToEmptyLines } from "../../pages/ViewPublicationPage/tools/replaceSpacesToEmptyLines";
@ -21,14 +20,13 @@ async function getQuizData(quizId: string) {
const quizDataResponse = response.data; const quizDataResponse = response.data;
if (response.error) { if (response.error) {
enqueueSnackbar(response.error);
throw new Error(response.error); throw new Error(response.error);
} }
if (!quizDataResponse) { if (!quizDataResponse) {
throw new Error("Quiz not found"); throw new Error("Quiz not found");
} }
const quizSettings = replaceSpacesToEmptyLines(parseQuizData(quizDataResponse, quizId)); const quizSettings = replaceSpacesToEmptyLines(parseQuizData(quizDataResponse));
return JSON.parse(JSON.stringify({ data: quizSettings }).replaceAll(/\\" \\"/g, '""').replaceAll(/" "/g, '""')).data as QuizSettings; return JSON.parse(JSON.stringify({ data: quizSettings }).replaceAll(/\\" \\"/g, '""').replaceAll(/" "/g, '""')).data as QuizSettings;
} }