Merge branch 'dev' into 'staging'

split widget and default App components

See merge request frontend/squzanswerer!50
This commit is contained in:
Mikhail 2024-02-06 21:04:40 +00:00
commit 00323a1c6c
36 changed files with 1341 additions and 1334 deletions

@ -27,6 +27,8 @@
"notistack": "^3.0.1", "notistack": "^3.0.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-error-boundary": "^4.0.12",
"react-router-dom": "^6.21.3",
"swr": "^2.2.4", "swr": "^2.2.4",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"use-debounce": "^9.0.4", "use-debounce": "^9.0.4",

@ -1,56 +1,21 @@
import { Box, CssBaseline, ThemeProvider } from "@mui/material"; import { Box } from "@mui/material";
import { LocalizationProvider } from "@mui/x-date-pickers"; import { useParams } from "react-router-dom";
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment"; import QuizAnswerer from "./QuizAnswerer";
import { ruRU } from '@mui/x-date-pickers/locales'; import { QuizIdContext } from "./contexts/QuizIdContext";
import moment from "moment";
import { SnackbarProvider } from 'notistack';
import { SWRConfig } from "swr";
import { ViewPage } from "./pages/ViewPublicationPage/ViewPublicationPage";
import lightTheme from "./utils/themes/light";
const defaultQuizId = "ef836ff8-35b1-4031-9acf-af5766bac2b2"; const defaultQuizId = "ef836ff8-35b1-4031-9acf-af5766bac2b2";
moment.locale("ru"); export default function App() {
const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeText; const quizId = useParams().quizId ?? defaultQuizId;
interface Props {
widget?: boolean;
quizId?: string;
}
export default function App({ widget = false, quizId }: Props) {
quizId ??= defaultQuizId;
return ( return (
<SWRConfig value={{ <QuizIdContext.Provider value={quizId}>
revalidateOnFocus: false, <Box sx={{
shouldRetryOnError: false, height: "100dvh",
}}> }}>
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="ru" localeText={localeText}> <QuizAnswerer />
<ThemeProvider theme={lightTheme}> </Box>
<SnackbarProvider </QuizIdContext.Provider>
preventDuplicate={true}
style={{ backgroundColor: lightTheme.palette.brightPurple.main }}
>
<CssBaseline />
{widget ? (
<Box sx={{
width: "100%",
height: "100%",
}}>
<ViewPage quizId={quizId} />
</Box>
) : (
<Box sx={{
height: "100dvh",
}}>
<ViewPage quizId={quizId} />
</Box>
)}
</SnackbarProvider>
</ThemeProvider>
</LocalizationProvider>
</SWRConfig>
); );
} }

47
src/QuizAnswerer.tsx Normal file

@ -0,0 +1,47 @@
import { CssBaseline, ThemeProvider } from "@mui/material";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
import { ruRU } from '@mui/x-date-pickers/locales';
import LoadingSkeleton from "@ui_kit/LoadingSkeleton";
import { handleComponentError } from "@utils/handleComponentError";
import moment from "moment";
import { SnackbarProvider } from 'notistack';
import { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { SWRConfig } from "swr";
import { ApologyPage } from "./pages/ViewPublicationPage/ApologyPage";
import { ViewPage } from "./pages/ViewPublicationPage/ViewPublicationPage";
import lightTheme from "./utils/themes/light";
moment.locale("ru");
const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeText;
export default function QuizAnswerer() {
return (
<SWRConfig value={{
revalidateOnFocus: false,
shouldRetryOnError: false,
}}>
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="ru" localeText={localeText}>
<ThemeProvider theme={lightTheme}>
<SnackbarProvider
preventDuplicate={true}
style={{ backgroundColor: lightTheme.palette.brightPurple.main }}
>
<CssBaseline />
<ErrorBoundary
fallback={<ApologyPage message="Что-то пошло не так" />}
onError={handleComponentError}
>
<Suspense fallback={<LoadingSkeleton />}>
<ViewPage />
</Suspense>
</ErrorBoundary>
</SnackbarProvider>
</ThemeProvider>
</LocalizationProvider>
</SWRConfig>
);
}

22
src/WidgetApp.tsx Normal file

@ -0,0 +1,22 @@
import { Box } from "@mui/material";
import QuizAnswerer from "./QuizAnswerer";
import { QuizIdContext } from "./contexts/QuizIdContext";
interface Props {
quizId: string;
}
export default function WidgetApp({ quizId }: Props) {
return (
<QuizIdContext.Provider value={quizId}>
<Box sx={{
width: "100%",
height: "100%",
}}>
<QuizAnswerer />
</Box>
</QuizIdContext.Provider>
);
}

@ -0,0 +1,10 @@
export default function BlankImage() {
return (
<svg width="100%" height="100%" viewBox="0 -70 800 535" fill="none" display="block" preserveAspectRatio="xMidYMax meet" xmlns="http://www.w3.org/2000/svg">
<path fill="#F0F0F0" d="M555 47a47.003 47.003 0 0 1 29.014-43.422 46.999 46.999 0 0 1 61.408 61.408 46.997 46.997 0 0 1-76.656 15.248A47 47 0 0 1 555 47Z" />
<path fill="#F3F3F3" d="M641.874 240.665c7.74-7.74 20.263-7.82 28.102-.181L1051 611.837 779.035 883.805 383.869 498.67l258.005-258.005Z" />
<path fill="#EDEDED" d="M183.393 61.546c7.692-7.037 19.499-6.985 27.129.12l677.42 630.746-690.929 382.738L-397 592.531 183.393 61.546Z" />
</svg>
);
}

@ -0,0 +1,11 @@
import { createContext, useContext } from "react";
export const QuizIdContext = createContext<string | null>(null);
export const useQuizId = () => {
const quizId = useContext(QuizIdContext);
if (quizId === null) throw new Error("quizId context is null");
return quizId;
};

@ -1,7 +1,24 @@
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import App from "./App"; import App from "./App";
const router = createBrowserRouter([
{
path: "/",
children: [
{
index: true,
element: <App />,
},
{
path: ":quizId",
element: <App />,
},
]
}
]);
const root = createRoot(document.getElementById("root")!); const root = createRoot(document.getElementById("root")!);
root.render(<App />); root.render(<RouterProvider router={router} />);

@ -24,8 +24,8 @@ export interface GetQuizDataResponse {
}[]; }[];
} }
export function parseQuizData(quizDataResponse: GetQuizDataResponse, quizId: string): QuizSettings { export function parseQuizData(quizDataResponse: GetQuizDataResponse, quizId: string): Omit<QuizSettings, "recentlyCompleted"> {
const items: QuizSettings["items"] = quizDataResponse.items.map((item) => { const items: QuizSettings["questions"] = quizDataResponse.items.map((item) => {
const content = JSON.parse(item.c); const content = JSON.parse(item.c);
return { return {
@ -51,5 +51,5 @@ export function parseQuizData(quizDataResponse: GetQuizDataResponse, quizId: str
pausable: quizDataResponse.settings.pausable pausable: quizDataResponse.settings.pausable
}; };
return { cnt: quizDataResponse.cnt, settings, items }; return { cnt: quizDataResponse.cnt, settings, questions: items };
} }

@ -30,7 +30,7 @@ export type FCField = {
}; };
export type QuizSettings = { export type QuizSettings = {
items: AnyTypedQuizQuestion[]; questions: AnyTypedQuizQuestion[];
settings: { settings: {
qid: string; qid: string;
fp: boolean; fp: boolean;
@ -43,6 +43,7 @@ export type QuizSettings = {
cfg: QuizConfig; cfg: QuizConfig;
}; };
cnt: number; cnt: number;
recentlyCompleted: boolean;
}; };
export interface QuizConfig { export interface QuizConfig {

@ -1,22 +1,21 @@
import AddressIcon from "@icons/ContactFormIcon/AddressIcon"; import AddressIcon from "@icons/ContactFormIcon/AddressIcon";
import NameIcon from "@icons/ContactFormIcon/NameIcon";
import EmailIcon from "@icons/ContactFormIcon/EmailIcon"; import EmailIcon from "@icons/ContactFormIcon/EmailIcon";
import NameIcon from "@icons/ContactFormIcon/NameIcon";
import PhoneIcon from "@icons/ContactFormIcon/PhoneIcon"; import PhoneIcon from "@icons/ContactFormIcon/PhoneIcon";
import TextIcon from "@icons/ContactFormIcon/TextIcon"; import TextIcon from "@icons/ContactFormIcon/TextIcon";
import { Box, Button, InputAdornment, Link, TextField as MuiTextField, TextFieldProps, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Button, InputAdornment, Link, TextField as MuiTextField, TextFieldProps, Typography, useMediaQuery, useTheme } from "@mui/material";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import { FC, useRef, useState } from "react"; 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 { quizThemes } from "@utils/themes/Publication/themePublication"; import { useQuizData } from "@utils/hooks/useQuizData";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { ApologyPage } from "./ApologyPage"; import { ApologyPage } from "./ApologyPage";
import { checkEmptyData } from "./tools/checkEmptyData"; import { checkEmptyData } from "./tools/checkEmptyData";
import { useQuestionsStore } from "@stores/quizData/store"; import { quizThemes } from "@utils/themes/Publication/themePublication";
const TextField = MuiTextField as unknown as FC<TextFieldProps>; // temporary fix ts(2590) const TextField = MuiTextField as unknown as FC<TextFieldProps>; // temporary fix ts(2590)
@ -74,7 +73,7 @@ export const ContactForm = ({
setShowResultForm, setShowResultForm,
}: ContactFormProps) => { }: ContactFormProps) => {
const theme = useTheme(); const theme = useTheme();
const { settings, items } = useQuestionsStore(); const { settings, questions } = useQuizData();
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
const [name, setName] = useState(""); const [name, setName] = useState("");
@ -93,7 +92,7 @@ export const ContactForm = ({
}; };
//@ts-ignore //@ts-ignore
const resultQuestion: QuizQuestionResult = items.find((question) => { const resultQuestion: QuizQuestionResult = questions.find((question) => {
if (settings?.cfg.haveRoot) { if (settings?.cfg.haveRoot) {
//ветвимся //ветвимся
return ( return (
@ -110,8 +109,6 @@ export const ContactForm = ({
}); });
const inputHC = async () => { const inputHC = async () => {
if (!settings) return;
//@ts-ignore //@ts-ignore
const FC = settings?.cfg.formContact.fields || settings?.cfg.formContact; const FC = settings?.cfg.formContact.fields || settings?.cfg.formContact;
const body = {}; const body = {};
@ -157,7 +154,6 @@ export const ContactForm = ({
} }
let isWide = Object.keys(filteredFC).length > 2; let isWide = Object.keys(filteredFC).length > 2;
if (!settings) throw new Error("settings is null");
if (!resultQuestion) if (!resultQuestion)
return ( return (
<ApologyPage message="не получилось найти результат для этой ветки :(" /> <ApologyPage message="не получилось найти результат для этой ветки :(" />
@ -360,15 +356,13 @@ export const ContactForm = ({
<NameplateLogo <NameplateLogo
style={{ style={{
fontSize: "34px", fontSize: "34px",
//@ts-ignore color: quizThemes[settings.cfg.theme].isLight ? "#151515" : "#FFFFFF",
color: mode[settings.cfg.theme] ? "#151515" : "#FFFFFF",
}} }}
/> />
<Typography <Typography
sx={{ sx={{
fontSize: "20px", fontSize: "20px",
//@ts-ignore color: quizThemes[settings.cfg.theme].isLight ? "#4D4D4D" : "#F5F7FF",
color: mode[settings.cfg.theme] ? "#4D4D4D" : "#F5F7FF",
whiteSpace: "nowrap", whiteSpace: "nowrap",
}} }}
> >
@ -393,7 +387,7 @@ const Inputs = ({
adress, adress,
setAdress, setAdress,
}: any) => { }: any) => {
const { settings } = useQuestionsStore(); 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;

@ -1,16 +1,14 @@
import { Box, Button, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Button, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { getQuestionById } from "@stores/quizData/actions";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import type { AnyTypedQuizQuestion, QuizQuestionBase } from "../../model/questionTypes/shared"; import type { AnyTypedQuizQuestion, QuizQuestionBase } from "../../model/questionTypes/shared";
import { checkEmptyData } from "./tools/checkEmptyData"; import { checkEmptyData } from "./tools/checkEmptyData";
import type { QuizQuestionResult } from "@model/questionTypes/result"; import type { QuizQuestionResult } from "@model/questionTypes/result";
import { useQuestionsStore } from "@stores/quizData/store";
import { useQuizViewStore } from "@stores/quizView/store"; import { useQuizViewStore } from "@stores/quizView/store";
import { useQuizData } from "@utils/hooks/useQuizData";
type FooterProps = { type FooterProps = {
setCurrentQuestion: (step: AnyTypedQuizQuestion) => void; setCurrentQuestion: (step: AnyTypedQuizQuestion) => void;
@ -22,13 +20,13 @@ type FooterProps = {
export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setShowResultForm }: FooterProps) => { export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setShowResultForm }: FooterProps) => {
const theme = useTheme(); const theme = useTheme();
const { settings, items } = useQuestionsStore(); const { settings, questions } = useQuizData();
const answers = useQuizViewStore(state => state.answers); const answers = useQuizViewStore(state => state.answers);
const [stepNumber, setStepNumber] = useState(1); const [stepNumber, setStepNumber] = useState(1);
const isMobileMini = useMediaQuery(theme.breakpoints.down(382)); const isMobileMini = useMediaQuery(theme.breakpoints.down(382));
const isLinear = !items.some(({ content }) => content.rule.parentId === "root"); const isLinear = !questions.some(({ content }) => content.rule.parentId === "root");
const getNextQuestionId = useCallback(() => { const getNextQuestionId = useCallback(() => {
console.log("Смотрим какой вопрос будет дальше. Что у нас сегодня вкусненького? Щя покажу от какого вопроса мы ищем следующий шаг"); console.log("Смотрим какой вопрос будет дальше. Что у нас сегодня вкусненького? Щя покажу от какого вопроса мы ищем следующий шаг");
@ -86,27 +84,27 @@ export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setSh
} }
//ничё не нашли, ищем резулт //ничё не нашли, ищем резулт
console.log("ничё не нашли, ищем резулт "); console.log("ничё не нашли, ищем резулт ");
return items.find(q => { return questions.find(q => {
console.log('q.type === "result"', q.type === "result"); console.log('q.type === "result"', q.type === "result");
console.log('q.content.rule.parentId', q.content.rule.parentId); console.log('q.content.rule.parentId', q.content.rule.parentId);
console.log('question.content.id', question.content.id); console.log('question.content.id', question.content.id);
return q.type === "result" && q.content.rule.parentId === question.content.id; return q.type === "result" && q.content.rule.parentId === question.content.id;
})?.id; })?.id;
}, [answers, items, question]); }, [answers, questions, question]);
const isPreviousButtonDisabled = useMemo(() => { const isPreviousButtonDisabled = useMemo(() => {
// Логика для аргумента disabled у кнопки "Назад" // Логика для аргумента disabled у кнопки "Назад"
if (isLinear) { if (isLinear) {
const questionIndex = items.findIndex(({ id }) => id === question.id); const questionIndex = questions.findIndex(({ id }) => id === question.id);
const previousQuestion = items[questionIndex - 1]; const previousQuestion = questions[questionIndex - 1];
return previousQuestion ? false : true; return previousQuestion ? false : true;
} else { } else {
return question?.content.rule.parentId === "root" ? true : false; return question?.content.rule.parentId === "root" ? true : false;
} }
}, [items, isLinear, question?.content.rule.parentId, question.id]); }, [questions, isLinear, question?.content.rule.parentId, question.id]);
const isNextButtonDisabled = useMemo(() => { const isNextButtonDisabled = useMemo(() => {
// Логика для аргумента disabled у кнопки "Далее" // Логика для аргумента disabled у кнопки "Далее"
@ -128,7 +126,8 @@ export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setSh
if (nextQuestionId) { if (nextQuestionId) {
return false; return false;
} else { } else {
const nextQuestion = getQuestionById(question.content.rule.default); const questionId = question.content.rule.default;
const nextQuestion = questions.find(q => q.id === questionId || q.content.id === questionId) || null;
if (nextQuestion?.type) { if (nextQuestion?.type) {
return false; return false;
@ -137,7 +136,6 @@ export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setSh
}, [answers, getNextQuestionId, isLinear, question.content, question.id]); }, [answers, getNextQuestionId, isLinear, question.content, question.id]);
const showResult = (nextQuestion: QuizQuestionResult) => { const showResult = (nextQuestion: QuizQuestionResult) => {
if (!settings) return;
if (!nextQuestion) return; if (!nextQuestion) return;
const isEmpty = checkEmptyData({ resultData: nextQuestion }); const isEmpty = checkEmptyData({ resultData: nextQuestion });
@ -168,9 +166,9 @@ export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setSh
if (isLinear) { if (isLinear) {
setStepNumber(q => q - 1); setStepNumber(q => q - 1);
const questionIndex = items.findIndex(({ id }) => id === question.id); const questionIndex = questions.findIndex(({ id }) => id === question.id);
const previousQuestion = items[questionIndex - 1]; const previousQuestion = questions[questionIndex - 1];
if (previousQuestion) { if (previousQuestion) {
setCurrentQuestion(previousQuestion); setCurrentQuestion(previousQuestion);
@ -180,7 +178,8 @@ export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setSh
} }
if (question?.content.rule.parentId !== "root") { if (question?.content.rule.parentId !== "root") {
const parent = getQuestionById(question?.content.rule.parentId); const questionId = question?.content.rule.parentId;
const parent = questions.find(q => q.id === questionId || q.content.id === questionId) || null;
if (parent?.type) { if (parent?.type) {
setCurrentQuestion(parent); setCurrentQuestion(parent);
} else { } else {
@ -195,14 +194,14 @@ export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setSh
if (isLinear) { if (isLinear) {
setStepNumber(q => q + 1); setStepNumber(q => q + 1);
const questionIndex = items.findIndex(({ id }) => id === question.id); const questionIndex = questions.findIndex(({ id }) => id === question.id);
const nextQuestion = items[questionIndex + 1]; const nextQuestion = questions[questionIndex + 1];
if (nextQuestion && nextQuestion.type !== "result") { if (nextQuestion && nextQuestion.type !== "result") {
setCurrentQuestion(nextQuestion); setCurrentQuestion(nextQuestion);
} else { } else {
//@ts-ignore //@ts-ignore
showResult(items.find(q => q.content.rule.parentId === "line")); showResult(questions.find(q => q.content.rule.parentId === "line"));
} }
return; return;
@ -211,7 +210,7 @@ export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setSh
const nextQuestionId = getNextQuestionId(); const nextQuestionId = getNextQuestionId();
if (nextQuestionId) { if (nextQuestionId) {
const nextQuestion = getQuestionById(nextQuestionId); const nextQuestion = questions.find(q => q.id === nextQuestionId || q.content.id === nextQuestionId) || null;
if (nextQuestion?.type && nextQuestion.type === "result") { if (nextQuestion?.type && nextQuestion.type === "result") {
showResult(nextQuestion); showResult(nextQuestion);
@ -278,7 +277,7 @@ export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setSh
</Typography> </Typography>
<Typography>Из</Typography> <Typography>Из</Typography>
<Typography sx={{ fontWeight: "bold" }}> <Typography sx={{ fontWeight: "bold" }}>
{items.filter(q => q.type !== "result").length} {questions.filter(q => q.type !== "result").length}
</Typography> </Typography>
</Box> </Box>
} }

@ -1,8 +1,6 @@
import { Box, useMediaQuery, useTheme } from "@mui/material"; import { Box, useMediaQuery, useTheme } from "@mui/material";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { getQuestionById } from "@stores/quizData/actions";
import { ContactForm } from "./ContactForm"; import { ContactForm } from "./ContactForm";
import { Footer } from "./Footer"; import { Footer } from "./Footer";
import { ResultForm } from "./ResultForm"; import { ResultForm } from "./ResultForm";
@ -23,23 +21,22 @@ import type { AnyTypedQuizQuestion } 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 { QuizQuestionResult } from "@model/questionTypes/result";
import { useQuestionsStore } from "@stores/quizData/store";
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 { useQuizData } from "@utils/hooks/useQuizData";
export const Question = () => { export const Question = () => {
const theme = useTheme(); const theme = useTheme();
const settings = useQuestionsStore(state => state.settings); const { settings, questions } = useQuizData();
const questions = useQuestionsStore(state => state.items);
const [currentQuestion, setCurrentQuestion] = useState<AnyTypedQuizQuestion>(); const [currentQuestion, setCurrentQuestion] = useState<AnyTypedQuizQuestion>();
const [showContactForm, setShowContactForm] = useState<boolean>(false); const [showContactForm, setShowContactForm] = useState<boolean>(false);
const [showResultForm, setShowResultForm] = useState<boolean>(false); const [showResultForm, setShowResultForm] = useState<boolean>(false);
const isMobile = useMediaQuery(theme.breakpoints.down(650)); const isMobile = useMediaQuery(theme.breakpoints.down(650));
useEffect(() => { useEffect(() => {
if (settings?.cfg.haveRoot) {//ветвимся if (settings?.cfg.haveRoot) {//ветвимся
const nextQuestion = getQuestionById(settings?.cfg.haveRoot || ""); const questionId = settings?.cfg.haveRoot || "";
const nextQuestion = questions.find(q => q.id === questionId || q.content.id === questionId) || null;
if (nextQuestion?.type) { if (nextQuestion?.type) {
setCurrentQuestion(nextQuestion); setCurrentQuestion(nextQuestion);
@ -49,10 +46,8 @@ export const Question = () => {
} else {//идём прямо } else {//идём прямо
setCurrentQuestion(questions[0]); setCurrentQuestion(questions[0]);
} }
}, []); }, []);
if (!settings) throw new Error("settings is null");
if (!currentQuestion || currentQuestion.type === "result") return "не смог отобразить вопрос"; if (!currentQuestion || currentQuestion.type === "result") return "не смог отобразить вопрос";
return ( return (

@ -9,10 +9,10 @@ import {
import { NameplateLogo } from "@icons/NameplateLogo"; import { NameplateLogo } from "@icons/NameplateLogo";
import YoutubeEmbedIframe from "./tools/YoutubeEmbedIframe"; import YoutubeEmbedIframe from "./tools/YoutubeEmbedIframe";
import { useQuestionsStore } from "@stores/quizData/store";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import { useCallback, useEffect, useMemo } from "react"; import { useCallback, useEffect, useMemo } from "react";
import type { QuizQuestionResult } from "../../model/questionTypes/result"; import type { QuizQuestionResult } from "../../model/questionTypes/result";
import { useQuizData } from "@utils/hooks/useQuizData";
type ResultFormProps = { type ResultFormProps = {
@ -29,34 +29,33 @@ export const ResultForm = ({
}: ResultFormProps) => { }: ResultFormProps) => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(650)); const isMobile = useMediaQuery(theme.breakpoints.down(650));
const { settings, items } = useQuestionsStore(); const { settings, questions } = useQuizData();
if (!settings) throw new Error("settings is null");
const resultQuestion = useMemo(() => { const resultQuestion = useMemo(() => {
if (settings?.cfg.haveRoot) { if (settings?.cfg.haveRoot) {
//ищём для ветвления //ищём для ветвления
return (items.find( return (questions.find(
(question): question is QuizQuestionResult => (question): question is QuizQuestionResult =>
question.type === "result" && question.type === "result" &&
question.content.rule.parentId === currentQuestion.content.id question.content.rule.parentId === currentQuestion.content.id
) || items.find( ) || questions.find(
(question): question is QuizQuestionResult => (question): question is QuizQuestionResult =>
question.type === "result" && question.type === "result" &&
question.content.rule.parentId === "line" question.content.rule.parentId === "line"
)); ));
} else { } else {
return items.find( return questions.find(
(question): question is QuizQuestionResult => (question): question is QuizQuestionResult =>
question.type === "result" && question.type === "result" &&
question.content.rule.parentId === "line" question.content.rule.parentId === "line"
); );
} }
}, [currentQuestion.content.id, items, settings?.cfg.haveRoot]); }, [currentQuestion.content.id, questions, settings?.cfg.haveRoot]);
const followNextForm = useCallback(() => { const followNextForm = useCallback(() => {
setShowResultForm(false); setShowResultForm(false);
setShowContactForm(true); setShowContactForm(true);
},[setShowContactForm, setShowResultForm]); }, [setShowContactForm, setShowResultForm]);
useEffect(() => { useEffect(() => {
if (!resultQuestion) { if (!resultQuestion) {

@ -1,12 +1,12 @@
import { Box, Button, ButtonBase, Link, Paper, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Button, ButtonBase, Link, Paper, Typography, useMediaQuery, useTheme } from "@mui/material";
import YoutubeEmbedIframe from "./tools/YoutubeEmbedIframe";
import { notReachable } from "../../utils/notReachable";
import { useUADevice } from "../../utils/hooks/useUADevice"; import { useUADevice } from "../../utils/hooks/useUADevice";
import { notReachable } from "../../utils/notReachable";
import YoutubeEmbedIframe from "./tools/YoutubeEmbedIframe";
import { NameplateLogo } from "@icons/NameplateLogo"; import { NameplateLogo } from "@icons/NameplateLogo";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { QuizStartpageAlignType, QuizStartpageType } from "@model/settingsData"; import { QuizStartpageAlignType, QuizStartpageType } from "@model/settingsData";
import { useQuestionsStore } from "@stores/quizData/store"; import { useQuizData } from "@utils/hooks/useQuizData";
import { quizThemes } from "@utils/themes/Publication/themePublication";
interface Props { interface Props {
@ -15,12 +15,10 @@ interface Props {
export const StartPageViewPublication = ({ setVisualStartPage }: Props) => { export const StartPageViewPublication = ({ setVisualStartPage }: Props) => {
const theme = useTheme(); const theme = useTheme();
const { settings } = useQuestionsStore(); const { settings } = useQuizData();
const { isMobileDevice } = useUADevice(); const { isMobileDevice } = useUADevice();
const isMobile = useMediaQuery(theme.breakpoints.down(650)); const isMobile = useMediaQuery(theme.breakpoints.down(650));
if (!settings) throw new Error("settings is null");
const handleCopyNumber = () => { const handleCopyNumber = () => {
navigator.clipboard.writeText(settings.cfg.info.phonenumber); navigator.clipboard.writeText(settings.cfg.info.phonenumber);
}; };

@ -1,56 +1,33 @@
import { getData } from "@api/quizRelase";
import { QuizSettings } from "@model/settingsData";
import { Box, ThemeProvider } from "@mui/material"; import { Box, ThemeProvider } from "@mui/material";
import { setQuizData } from "@stores/quizData/actions"; import { useQuizData } from "@utils/hooks/useQuizData";
import { useQuestionsStore } from "@stores/quizData/store";
import LoadingSkeleton from "@ui_kit/LoadingSkeleton";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import { enqueueSnackbar } from "notistack";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import useSWR from "swr";
import { ApologyPage } from "./ApologyPage"; import { ApologyPage } from "./ApologyPage";
import { Question } from "./Question"; import { Question } from "./Question";
import { StartPageViewPublication } from "./StartPageViewPublication"; import { StartPageViewPublication } from "./StartPageViewPublication";
import { parseQuizData } from "@model/api/getQuizData";
import { replaceSpacesToEmptyLines } from "./tools/replaceSpacesToEmptyLines";
type Props = { export const ViewPage = () => {
quizId: string; const { settings, questions, recentlyCompleted } = useQuizData();
};
export const ViewPage = ({ quizId }: Props) => {
const { isLoading, error } = useSWR(["quizData", quizId], params => getQuizData(params[1]), {
onSuccess: setQuizData,
});
const { settings, items, recentlyСompleted } = useQuestionsStore();
const [visualStartPage, setVisualStartPage] = useState<boolean>(); const [visualStartPage, setVisualStartPage] = useState<boolean>();
useEffect(() => {//установка фавиконки useEffect(() => {
if (!settings) return;
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); setVisualStartPage(!settings.cfg.noStartPage);
}, [settings]); }, [settings]);
const questionsCount = items.filter(({ type }) => type !== null && type !== "result").length; const questionsCount = questions.filter(({ type }) => type !== null && type !== "result").length;
if (error) {
console.log(error);
return <ApologyPage message="Что-то пошло не так" />;
}
if (isLoading || !settings) return <LoadingSkeleton />;
if (questionsCount === 0) return <ApologyPage message="Нет созданных вопросов" />; if (questionsCount === 0) return <ApologyPage message="Нет созданных вопросов" />;
return ( return (
<ThemeProvider theme={quizThemes[settings.cfg.theme || "StandardTheme"]}> <ThemeProvider theme={quizThemes[settings.cfg.theme || "StandardTheme"]}>
{recentlyСompleted ? ( {recentlyCompleted ? (
<ApologyPage message="Вы уже прошли этот опрос" /> <ApologyPage message="Вы уже прошли этот опрос" />
) : ( ) : (
<Box> <Box>
@ -63,21 +40,4 @@ export const ViewPage = ({ quizId }: Props) => {
)} )}
</ThemeProvider> </ThemeProvider>
); );
}; };
async function getQuizData(quizId: string) {
const response = await getData(quizId);
const quizDataResponse = response.data;
if (response.error) {
enqueueSnackbar(response.error);
throw new Error(response.error);
}
if (!quizDataResponse) {
throw new Error("Quiz not found");
}
const quizSettings = replaceSpacesToEmptyLines(parseQuizData(quizDataResponse, quizId));
return JSON.parse(JSON.stringify({ data: quizSettings }).replaceAll(/\\" \\"/g, '""').replaceAll(/" "/g, '""')).data as QuizSettings & { recentlyСompleted: boolean; };
}

@ -8,103 +8,101 @@ import type { QuizQuestionDate } from "../../../model/questionTypes/date";
import CalendarIcon from "@icons/CalendarIcon"; import CalendarIcon from "@icons/CalendarIcon";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { sendAnswer } from "@api/quizRelase"; import { sendAnswer } from "@api/quizRelase";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import { useQuestionsStore } from "@stores/quizData/store"; import { useQuizData } from "@utils/hooks/useQuizData";
type DateProps = { type DateProps = {
currentQuestion: QuizQuestionDate; currentQuestion: QuizQuestionDate;
}; };
export const Date = ({ currentQuestion }: DateProps) => { export const Date = ({ currentQuestion }: DateProps) => {
const theme = useTheme(); const theme = useTheme();
const { settings } = useQuestionsStore(); const { settings } = useQuizData();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const answer = answers.find( const answer = answers.find(
({ questionId }) => questionId === currentQuestion.id ({ questionId }) => questionId === currentQuestion.id
)?.answer as string; )?.answer as string;
const currentAnswer = moment(answer) || moment(); const currentAnswer = moment(answer) || moment();
if (!settings) throw new Error("settings is null");
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary}> <Typography variant="h5" color={theme.palette.text.primary}>
{currentQuestion.title} {currentQuestion.title}
</Typography> </Typography>
<Box <Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
marginTop: "20px",
}}
>
<DatePicker
slots={{
openPickerIcon: () => (
<CalendarIcon
sx={{ sx={{
"& path": { stroke: theme.palette.primary.main }, display: "flex",
"& rect": { stroke: theme.palette.primary.main }, flexDirection: "column",
width: "100%",
marginTop: "20px",
}} }}
/> >
), <DatePicker
}} slots={{
value={ currentAnswer } openPickerIcon: () => (
onChange={async (date) => { <CalendarIcon
console.log(date) sx={{
if (!date) { "& path": { stroke: theme.palette.primary.main },
return; "& rect": { stroke: theme.palette.primary.main },
} }}
/>
),
}}
value={currentAnswer}
onChange={async (date) => {
console.log(date);
if (!date) {
return;
}
try { try {
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: settings.qid,
}); });
updateAnswer( updateAnswer(
currentQuestion.id, currentQuestion.id,
date date
); );
} catch (e) { } catch (e) {
enqueueSnackbar("ответ не был засчитан"); enqueueSnackbar("ответ не был засчитан");
} }
}} }}
slotProps={{ slotProps={{
openPickerButton: { openPickerButton: {
sx: { sx: {
p: 0, p: 0,
}, },
"data-cy": "open-datepicker", "data-cy": "open-datepicker",
}, },
layout: { layout: {
sx: { backgroundColor: theme.palette.background.default }, sx: { backgroundColor: theme.palette.background.default },
}, },
}} }}
sx={{ sx={{
"& .MuiInputBase-root": { "& .MuiInputBase-root": {
backgroundColor: quizThemes[settings.cfg.theme].isLight backgroundColor: quizThemes[settings.cfg.theme].isLight
? "white" ? "white"
: theme.palette.background.default, : theme.palette.background.default,
borderRadius: "10px", borderRadius: "10px",
maxWidth: "250px", maxWidth: "250px",
pr: "22px", pr: "22px",
"& input": { "& input": {
py: "11px", py: "11px",
pl: "20px", pl: "20px",
lineHeight: "19px", lineHeight: "19px",
}, },
"& fieldset": { "& fieldset": {
borderColor: "#9A9AAF", borderColor: "#9A9AAF",
}, },
}, },
}} }}
/> />
</Box> </Box>
</Box> </Box>
); );
}; };

@ -14,148 +14,146 @@ 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 { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import type { QuizQuestionEmoji } from "../../../model/questionTypes/emoji"; import type { QuizQuestionEmoji } from "../../../model/questionTypes/emoji";
import { useQuestionsStore } from "@stores/quizData/store"; import { useQuizData } from "@utils/hooks/useQuizData";
type EmojiProps = { type EmojiProps = {
currentQuestion: QuizQuestionEmoji; currentQuestion: QuizQuestionEmoji;
}; };
export const Emoji = ({ currentQuestion }: EmojiProps) => { export const Emoji = ({ currentQuestion }: EmojiProps) => {
const theme = useTheme(); const theme = useTheme();
const { settings } = useQuestionsStore()
const { answers } = useQuizViewStore();
const { answer } =
answers.find(
({ questionId }) => questionId === currentQuestion.id
) ?? {};
if (!settings) throw new Error("settings is null"); const { settings } = useQuizData();
const { answers } = useQuizViewStore();
const { answer } =
answers.find(
({ questionId }) => questionId === currentQuestion.id
) ?? {};
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography> <Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
<RadioGroup <RadioGroup
name={currentQuestion.id} name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex( value={currentQuestion.content.variants.findIndex(
({ id }) => answer === id ({ id }) => answer === id
)} )}
onChange={({ target }) =>{ onChange={({ target }) => {
updateAnswer( updateAnswer(
currentQuestion.id,
currentQuestion.content.variants[Number(target.value)].answer
)
}
}
sx={{
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
marginTop: "20px",
}}
>
<Box sx={{ display: "flex", width: "100%", gap: "42px", flexWrap: "wrap" }}>
{currentQuestion.content.variants.map((variant, index) => (
<FormControl
key={variant.id}
sx={{
borderRadius: "12px",
border: `1px solid`,
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
overflow: "hidden",
maxWidth: "317px",
width: "100%",
height: "255px",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
height: "193px",
background: "#ffffff",
}}
>
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
{variant.extendedText && (
<Typography fontSize={"100px"}>
{variant.extendedText}
</Typography>
)}
</Box>
</Box>
<FormControlLabel
key={variant.id}
sx={{
margin: 0,
padding: "15px",
color: theme.palette.text.primary,
display: "flex",
gap: "10px",
}}
value={index}
onClick={async (event) => {
event.preventDefault();
try {
await sendAnswer({
questionId: currentQuestion.id,
body: currentQuestion.content.variants[index].extendedText + " " + currentQuestion.content.variants[index].answer,
qid: settings.qid
})
updateAnswer(
currentQuestion.id, currentQuestion.id,
currentQuestion.content.variants[index].id currentQuestion.content.variants[Number(target.value)].answer
); );
}
} catch (e) { }
enqueueSnackbar("ответ не был засчитан") sx={{
} display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
if (answer === currentQuestion.content.variants[index].id) { marginTop: "20px",
deleteAnswer(currentQuestion.id); }}
try { >
<Box sx={{ display: "flex", width: "100%", gap: "42px", flexWrap: "wrap" }}>
await sendAnswer({ {currentQuestion.content.variants.map((variant, index) => (
questionId: currentQuestion.id, <FormControl
body: "", key={variant.id}
qid: settings.qid sx={{
}) borderRadius: "12px",
border: `1px solid`,
} catch (e) { borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
enqueueSnackbar("ответ не был засчитан") overflow: "hidden",
} maxWidth: "317px",
} width: "100%",
}} height: "255px",
}}
control={ >
<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main}/>} icon={<RadioIcon />} /> <Box
} sx={{
label={ display: "flex",
<Box sx={{ display: "flex", gap: "10px" }}> alignItems: "center",
<Typography sx={{wordBreak: "break-word"}}>{variant.answer}</Typography> height: "193px",
</Box> background: "#ffffff",
} }}
/> >
</FormControl> <Box
))} sx={{
</Box> width: "100%",
</RadioGroup> display: "flex",
</Box> justifyContent: "center",
}}
>
{variant.extendedText && (
<Typography fontSize={"100px"}>
{variant.extendedText}
</Typography>
)}
</Box>
</Box>
<FormControlLabel
key={variant.id}
sx={{
margin: 0,
padding: "15px",
color: theme.palette.text.primary,
display: "flex",
gap: "10px",
}}
value={index}
onClick={async (event) => {
event.preventDefault();
try {
await sendAnswer({
questionId: currentQuestion.id,
body: currentQuestion.content.variants[index].extendedText + " " + currentQuestion.content.variants[index].answer,
qid: settings.qid
});
updateAnswer(
currentQuestion.id,
currentQuestion.content.variants[index].id
);
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
if (answer === currentQuestion.content.variants[index].id) {
deleteAnswer(currentQuestion.id);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: settings.qid
});
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
}
}}
control={
<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main} />} icon={<RadioIcon />} />
}
label={
<Box sx={{ display: "flex", gap: "10px" }}>
<Typography sx={{ wordBreak: "break-word" }}>{variant.answer}</Typography>
</Box>
}
/>
</FormControl>
))}
</Box>
</RadioGroup>
</Box>
); );
}; };

@ -1,12 +1,11 @@
import { import {
Box, Box,
Typography, Typography,
ButtonBase, ButtonBase,
useTheme, useTheme,
IconButton, useMediaQuery, Modal, IconButton, useMediaQuery, Modal,
} from "@mui/material"; } from "@mui/material";
import { useQuizViewStore, updateAnswer } from "@stores/quizView/store"; import { useQuizViewStore, updateAnswer } from "@stores/quizView/store";
import { UPLOAD_FILE_TYPES_MAP } from "../tools/File";
import UploadIcon from "@icons/UploadIcon"; import UploadIcon from "@icons/UploadIcon";
import CloseBold from "@icons/CloseBold"; import CloseBold from "@icons/CloseBold";
@ -17,328 +16,326 @@ import type { DragEvent } from "react";
import type { UploadFileType } from "@model/questionTypes/file"; import type { UploadFileType } from "@model/questionTypes/file";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { sendAnswer, sendFile } from "@api/quizRelase"; import { sendAnswer, sendFile } from "@api/quizRelase";
import { useQuestionsStore } from "@stores/quizData/store"
import Info from "@icons/Info"; import Info from "@icons/Info";
import { useQuizData } from "@utils/hooks/useQuizData";
type FileProps = { type FileProps = {
currentQuestion: QuizQuestionFile; currentQuestion: QuizQuestionFile;
}; };
const CurrentModal = ({ status }: { status: "errorType" | "errorSize" | "picture" | "video" | "audio" | "document" | "" }) => { const CurrentModal = ({ status }: { status: "errorType" | "errorSize" | "picture" | "video" | "audio" | "document" | ""; }) => {
switch (status) { switch (status) {
case 'errorType': case 'errorType':
return (<> return (<>
<Typography>Выбран некорректный тип файла</Typography> <Typography>Выбран некорректный тип файла</Typography>
</>) </>);
case 'errorSize': case 'errorSize':
return (<> return (<>
<Typography>Файл слишком большой. Максимальный размер 50 МБ</Typography> <Typography>Файл слишком большой. Максимальный размер 50 МБ</Typography>
</>) </>);
default: default:
return (<> return (<>
<Typography>Допустимые расширения файлов:</Typography> <Typography>Допустимые расширения файлов:</Typography>
<Typography>{ <Typography>{
//@ts-ignore //@ts-ignore
ACCEPT_SEND_FILE_TYPES_MAP[status].join(" ")}</Typography> ACCEPT_SEND_FILE_TYPES_MAP[status].join(" ")}</Typography>
</>) </>);
} }
} };
const ACCEPT_SEND_FILE_TYPES_MAP = { const ACCEPT_SEND_FILE_TYPES_MAP = {
picture: [ picture: [
".jpeg", ".jpeg",
".jpg", ".jpg",
".png", ".png",
".ico", ".ico",
".gif", ".gif",
".tiff", ".tiff",
".webp", ".webp",
".eps", ".eps",
".svg" ".svg"
], ],
video: [ video: [
".mp4", ".mp4",
".mov", ".mov",
".wmv", ".wmv",
".avi", ".avi",
".avchd", ".avchd",
".flv", ".flv",
".f4v", ".f4v",
".swf", ".swf",
".mkv", ".mkv",
".webm", ".webm",
".mpeg-2" ".mpeg-2"
], ],
audio: [ audio: [
".aac", ".aac",
".aiff", ".aiff",
".dsd", ".dsd",
".flac", ".flac",
".mp3", ".mp3",
".mqa", ".mqa",
".ogg", ".ogg",
".wav", ".wav",
".wma" ".wma"
], ],
document: [ document: [
".doc", ".doc",
".docx", ".docx",
".dotx", ".dotx",
".rtf", ".rtf",
".odt", ".odt",
".pdf", ".pdf",
".txt", ".txt",
".xls", ".xls",
".ppt", ".ppt",
".xlsx", ".xlsx",
".pptx", ".pptx",
".pages", ".pages",
], ],
} };
const UPLOAD_FILE_DESCRIPTIONS_MAP: Record< const UPLOAD_FILE_DESCRIPTIONS_MAP: Record<
UploadFileType, UploadFileType,
{ title: string; description: string } { title: string; description: string; }
> = { > = {
picture: { picture: {
title: "Добавить изображение", title: "Добавить изображение",
description: "Принимает изображения", description: "Принимает изображения",
}, },
video: { video: {
title: "Добавить видео", title: "Добавить видео",
description: "Принимает .mp4 и .mov формат — максимум 100мб", description: "Принимает .mp4 и .mov формат — максимум 100мб",
}, },
audio: { title: "Добавить аудиофайл", description: "Принимает аудиофайлы" }, audio: { title: "Добавить аудиофайл", description: "Принимает аудиофайлы" },
document: { title: "Добавить документ", description: "Принимает документы" }, document: { title: "Добавить документ", description: "Принимает документы" },
} as const; } as const;
export const File = ({ currentQuestion }: FileProps) => { export const File = ({ currentQuestion }: FileProps) => {
const theme = useTheme(); const theme = useTheme();
const { settings } = useQuestionsStore() const { settings } = useQuizData();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
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(
({ questionId }) => questionId === currentQuestion.id ({ questionId }) => questionId === currentQuestion.id
)?.answer as string; )?.answer as string;
const isMobile = useMediaQuery(theme.breakpoints.down(500)); const isMobile = useMediaQuery(theme.breakpoints.down(500));
const uploadFile = async ({ target }: ChangeEvent<HTMLInputElement>) => { const uploadFile = async ({ target }: ChangeEvent<HTMLInputElement>) => {
if (!settings) return; const file = target.files?.[0];
if (file) {
const file = target.files?.[0]; if (file.size <= 52428800) {
if (file) { //проверяем на соответствие
if (file.size <= 52428800) { console.log(file.name.toLowerCase());
//проверяем на соответствие if (ACCEPT_SEND_FILE_TYPES_MAP[currentQuestion.content.type].find((ednding => {
console.log(file.name.toLowerCase()) console.log(ednding);
if (ACCEPT_SEND_FILE_TYPES_MAP[currentQuestion.content.type].find((ednding => { console.log(file.name.toLowerCase().endsWith(ednding));
console.log(ednding) return file.name.toLowerCase().endsWith(ednding);
console.log(file.name.toLowerCase().endsWith(ednding)) }))) {
return file.name.toLowerCase().endsWith(ednding)
}))) {
//Нужный формат //Нужный формат
console.log(file) console.log(file);
try { try {
const data = await sendFile({ const data = await sendFile({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: { body: {
file: file, file: file,
name: file.name name: file.name
}, },
qid: settings.qid qid: settings.qid
}) });
console.log(data) console.log(data);
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
//@ts-ignore //@ts-ignore
body: `https://storage.yandexcloud.net/squizanswer/${settings.qid}/${currentQuestion.id}/${data.data.fileIDMap[currentQuestion.id]}`, body: `https://storage.yandexcloud.net/squizanswer/${settings.qid}/${currentQuestion.id}/${data.data.fileIDMap[currentQuestion.id]}`,
//@ts-ignore //@ts-ignore
qid: settings.qid qid: settings.qid
}) });
updateAnswer( updateAnswer(
currentQuestion.id, currentQuestion.id,
`${file.name}|${URL.createObjectURL(file)}` `${file.name}|${URL.createObjectURL(file)}`
); );
} catch (e) { } catch (e) {
console.log(e) console.log(e);
enqueueSnackbar("ответ не был засчитан") enqueueSnackbar("ответ не был засчитан");
} }
} else { } else {
//неподходящий формат
setStatusModal("errorType");
}
} else {
setStatusModal("errorSize");
}
//неподходящий формат
setStatusModal("errorType")
} }
} else { };
setStatusModal("errorSize") return (
} <>
<Box>
} <Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
};
return (
<>
<Box>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
marginTop: "20px",
maxWidth: answer?.split("|")[0] ? "640px" : "600px",
}}
>
{answer?.split("|")[0] && (
<Box sx={{ display: "flex", alignItems: "center", gap: "15px" }}>
<Typography color={theme.palette.text.primary}>Вы загрузили:</Typography>
<Box
sx={{
padding: "5px 5px 5px 16px",
backgroundColor: theme.palette.primary.main,
borderRadius: "8px",
color: "#FFFFFF",
display: "flex",
alignItems: "center",
overflow: "hidden",
gap: "15px",
}}
>
<Typography
sx={{
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
}}
>
{answer?.split("|")[0]}</Typography>
<IconButton
sx={{ p: 0 }}
onClick={() => {
updateAnswer(currentQuestion.id, "");
}}
>
<CloseBold />
</IconButton>
</Box>
</Box>
)}
{!answer?.split("|")[0] && (
<Box sx={{
display: "flex",
alignItems: "center"
}}>
<ButtonBase component="label" sx={{ justifyContent: "flex-start", width: "100%" }}>
<input
onChange={uploadFile}
hidden
accept={ACCEPT_SEND_FILE_TYPES_MAP[currentQuestion.content.type].join(",")}
multiple
type="file"
/>
<Box <Box
onDragOver={(event: DragEvent<HTMLDivElement>) => sx={{
event.preventDefault() display: "flex",
} flexDirection: "column",
sx={{ width: "100%",
width: "100%", marginTop: "20px",
height: isMobile ? undefined : "120px", maxWidth: answer?.split("|")[0] ? "640px" : "600px",
display: "flex", }}
gap: "50px",
justifyContent: "flex-start",
alignItems: "center",
padding: "33px 44px 33px 55px",
backgroundColor: theme.palette.background.default,
border: `1px solid #9A9AAF`,
// border: `1px solid ${theme.palette.grey2.main}`,
borderRadius: "8px",
}}
> >
<UploadIcon /> {answer?.split("|")[0] && (
<Box> <Box sx={{ display: "flex", alignItems: "center", gap: "15px" }}>
<Typography <Typography color={theme.palette.text.primary}>Вы загрузили:</Typography>
sx={{ <Box
color: "#9A9AAF", sx={{
// color: theme.palette.grey2.main, padding: "5px 5px 5px 16px",
fontWeight: 500, backgroundColor: theme.palette.primary.main,
}} borderRadius: "8px",
> color: "#FFFFFF",
{ display: "flex",
UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type] alignItems: "center",
.title overflow: "hidden",
} gap: "15px",
</Typography> }}
<Typography >
sx={{ <Typography
color: "#9A9AAF", sx={{
// color: theme.palette.grey2.main, whiteSpace: "nowrap",
fontSize: "16px", textOverflow: "ellipsis",
lineHeight: "19px", overflow: "hidden",
}} }}
> >
{ {answer?.split("|")[0]}</Typography>
UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type] <IconButton
.description sx={{ p: 0 }}
} onClick={() => {
</Typography> updateAnswer(currentQuestion.id, "");
</Box> }}
>
<CloseBold />
</IconButton>
</Box>
</Box>
)}
{!answer?.split("|")[0] && (
<Box sx={{
display: "flex",
alignItems: "center"
}}>
<ButtonBase component="label" sx={{ justifyContent: "flex-start", width: "100%" }}>
<input
onChange={uploadFile}
hidden
accept={ACCEPT_SEND_FILE_TYPES_MAP[currentQuestion.content.type].join(",")}
multiple
type="file"
/>
<Box
onDragOver={(event: DragEvent<HTMLDivElement>) =>
event.preventDefault()
}
sx={{
width: "100%",
height: isMobile ? undefined : "120px",
display: "flex",
gap: "50px",
justifyContent: "flex-start",
alignItems: "center",
padding: "33px 44px 33px 55px",
backgroundColor: theme.palette.background.default,
border: `1px solid #9A9AAF`,
// border: `1px solid ${theme.palette.grey2.main}`,
borderRadius: "8px",
}}
>
<UploadIcon />
<Box>
<Typography
sx={{
color: "#9A9AAF",
// color: theme.palette.grey2.main,
fontWeight: 500,
}}
>
{
UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type]
.title
}
</Typography>
<Typography
sx={{
color: "#9A9AAF",
// color: theme.palette.grey2.main,
fontSize: "16px",
lineHeight: "19px",
}}
>
{
UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type]
.description
}
</Typography>
</Box>
</Box>
</ButtonBase>
<Info sx={{ width: "40px", height: "40px" }} color={theme.palette.primary.main} onClick={() => setStatusModal(currentQuestion.content.type)} />
</Box>
)}
{answer && currentQuestion.content.type === "picture" && (
<img
src={answer.split("|")[1]}
alt=""
style={{
marginTop: "15px",
maxWidth: "300px",
maxHeight: "300px",
}}
/>
)}
{answer && currentQuestion.content.type === "video" && (
<video
src={answer.split("|")[1]}
style={{
marginTop: "15px",
maxWidth: "300px",
maxHeight: "300px",
objectFit: "cover",
}}
/>
)}
</Box> </Box>
</ButtonBase>
<Info sx={{ width: "40px", height: "40px" }} color={theme.palette.primary.main} onClick={() => setStatusModal(currentQuestion.content.type)} />
</Box> </Box>
)} <Modal
{answer && currentQuestion.content.type === "picture" && ( open={Boolean(statusModal)}
<img onClose={() => setStatusModal("")}
src={answer.split("|")[1]} >
alt="" <Box sx={{
style={{ position: 'absolute',
marginTop: "15px", top: '50%',
maxWidth: "300px", left: '50%',
maxHeight: "300px", transform: 'translate(-50%, -50%)',
}} width: isMobile ? 300 : 400,
/> bgcolor: 'background.paper',
)} borderRadius: 3,
{answer && currentQuestion.content.type === "video" && ( boxShadow: 24,
<video p: 4,
src={answer.split("|")[1]} }}>
style={{ <CurrentModal status={statusModal} />
marginTop: "15px", </Box>
maxWidth: "300px", </Modal>
maxHeight: "300px", </>
objectFit: "cover", );
}}
/>
)}
</Box>
</Box>
<Modal
open={Boolean(statusModal)}
onClose={() => setStatusModal("")}
>
<Box sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: isMobile ? 300 : 400,
bgcolor: 'background.paper',
borderRadius: 3,
boxShadow: 24,
p: 4,
}}>
<CurrentModal status={statusModal} />
</Box>
</Modal>
</>
);
}; };

@ -1,11 +1,11 @@
import { import {
Box, Box,
Typography, Typography,
RadioGroup, RadioGroup,
FormControlLabel, FormControlLabel,
Radio, Radio,
useTheme, useTheme,
useMediaQuery, useMediaQuery,
} from "@mui/material"; } from "@mui/material";
import { useQuizViewStore, updateAnswer, deleteAnswer } from "@stores/quizView/store"; import { useQuizViewStore, updateAnswer, deleteAnswer } from "@stores/quizView/store";
@ -15,138 +15,136 @@ import RadioIcon from "@ui_kit/RadioIcon";
import type { QuizQuestionImages } from "../../../model/questionTypes/images"; import type { QuizQuestionImages } from "../../../model/questionTypes/images";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { sendAnswer } from "@api/quizRelase"; import { sendAnswer } from "@api/quizRelase";
import { useQuestionsStore } from "@stores/quizData/store" import { useQuizData } from "@utils/hooks/useQuizData";
type ImagesProps = { type ImagesProps = {
currentQuestion: QuizQuestionImages; currentQuestion: QuizQuestionImages;
}; };
export const Images = ({ currentQuestion }: ImagesProps) => { export const Images = ({ currentQuestion }: ImagesProps) => {
const { settings } = useQuestionsStore() const { settings } = useQuizData();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const theme = useTheme(); const theme = useTheme();
const { answer } = const { answer } =
answers.find( answers.find(
({ questionId }) => questionId === currentQuestion.id ({ questionId }) => questionId === currentQuestion.id
) ?? {}; ) ?? {};
const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isMobile = useMediaQuery(theme.breakpoints.down(500)); const isMobile = useMediaQuery(theme.breakpoints.down(500));
if (!settings) throw new Error("settings is null"); return (
<Box>
return ( <Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
<Box> <RadioGroup
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography> name={currentQuestion.id}
<RadioGroup value={currentQuestion.content.variants.findIndex(
name={currentQuestion.id} ({ id }) => answer === id
value={currentQuestion.content.variants.findIndex( )}
({ id }) => answer === id
)}
sx={{
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
marginTop: "20px",
}}
>
<Box
sx={{
display: "grid",
gap: "15px",
gridTemplateColumns: isTablet
? isMobile
? "repeat(1, 1fr)"
: "repeat(2, 1fr)"
: "repeat(3, 1fr)",
width: "100%",
}}
>
{currentQuestion.content.variants.map((variant, index) => (
<Box
key={index}
sx={{
cursor: "pointer",
borderRadius: "5px",
border: `1px solid`,
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
}}
onClick={async (event) => {
event.preventDefault();
try {
await sendAnswer({
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}"/>`,
qid: settings.qid
})
updateAnswer(
currentQuestion.id,
currentQuestion.content.variants[index].id
);
} catch (e) {
enqueueSnackbar("ответ не был засчитан")
}
if (answer === currentQuestion.content.variants[index].id) {
deleteAnswer(currentQuestion.id);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: settings.qid
})
} catch (e) {
enqueueSnackbar("ответ не был засчитан")
}
}
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
<Box sx={{ width: "100%", height: "300px" }}>
{variant.extendedText && (
<img
src={variant.extendedText}
alt=""
style={{
display: "block",
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
)}
</Box>
</Box>
<FormControlLabel
key={variant.id}
sx={{ sx={{
textAlign: "center", display: "flex",
color: theme.palette.text.primary, flexWrap: "wrap",
marginTop: "10px", flexDirection: "row",
marginLeft: 0, justifyContent: "space-between",
padding: "10px", marginTop: "20px",
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
},
}} }}
value={index} >
control={ <Box
<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main} />} icon={<RadioIcon />} /> sx={{
} display: "grid",
label={variant.answer} gap: "15px",
/> gridTemplateColumns: isTablet
</Box> ? isMobile
))} ? "repeat(1, 1fr)"
: "repeat(2, 1fr)"
: "repeat(3, 1fr)",
width: "100%",
}}
>
{currentQuestion.content.variants.map((variant, index) => (
<Box
key={index}
sx={{
cursor: "pointer",
borderRadius: "5px",
border: `1px solid`,
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
}}
onClick={async (event) => {
event.preventDefault();
try {
await sendAnswer({
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}"/>`,
qid: settings.qid
});
updateAnswer(
currentQuestion.id,
currentQuestion.content.variants[index].id
);
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
if (answer === currentQuestion.content.variants[index].id) {
deleteAnswer(currentQuestion.id);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: settings.qid
});
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
}
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
<Box sx={{ width: "100%", height: "300px" }}>
{variant.extendedText && (
<img
src={variant.extendedText}
alt=""
style={{
display: "block",
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
)}
</Box>
</Box>
<FormControlLabel
key={variant.id}
sx={{
textAlign: "center",
color: theme.palette.text.primary,
marginTop: "10px",
marginLeft: 0,
padding: "10px",
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
},
}}
value={index}
control={
<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main} />} icon={<RadioIcon />} />
}
label={variant.answer}
/>
</Box>
))}
</Box>
</RadioGroup>
</Box> </Box>
</RadioGroup> );
</Box>
);
}; };

@ -12,14 +12,14 @@ import { enqueueSnackbar } from "notistack";
import { sendAnswer } from "@api/quizRelase"; import { sendAnswer } from "@api/quizRelase";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import { useQuestionsStore } from "@stores/quizData/store"; import { useQuizData } from "@utils/hooks/useQuizData";
type NumberProps = { type NumberProps = {
currentQuestion: QuizQuestionNumber; currentQuestion: QuizQuestionNumber;
}; };
export const Number = ({ currentQuestion }: NumberProps) => { export const Number = ({ currentQuestion }: NumberProps) => {
const { settings } = useQuestionsStore(); 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");
const [maxRange, setMaxRange] = useState<string>("100000000000"); const [maxRange, setMaxRange] = useState<string>("100000000000");
@ -105,8 +105,6 @@ export const Number = ({ currentQuestion }: NumberProps) => {
} }
}, []); }, []);
if (!settings) throw new Error("settings is null");
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary}> <Typography variant="h5" color={theme.palette.text.primary}>

@ -1,9 +1,9 @@
import { import {
Box, Box,
Typography, Typography,
Rating as RatingComponent, Rating as RatingComponent,
useTheme, useTheme,
useMediaQuery useMediaQuery
} from "@mui/material"; } from "@mui/material";
import { useQuizViewStore, updateAnswer } from "@stores/quizView/store"; import { useQuizViewStore, updateAnswer } from "@stores/quizView/store";
@ -19,127 +19,125 @@ import StarIconMini from "@icons/questionsPage/StarIconMini";
import type { QuizQuestionRating } from "../../../model/questionTypes/rating"; import type { QuizQuestionRating } from "../../../model/questionTypes/rating";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { sendAnswer } from "@api/quizRelase"; import { sendAnswer } from "@api/quizRelase";
import { useQuestionsStore } from "@stores/quizData/store"; import { useQuizData } from "@utils/hooks/useQuizData";
type RatingProps = { type RatingProps = {
currentQuestion: QuizQuestionRating; currentQuestion: QuizQuestionRating;
}; };
const buttonRatingForm = [ const buttonRatingForm = [
{ {
name: "star", name: "star",
icon: (color: string) => <StarIconMini width={50} color={color} />, icon: (color: string) => <StarIconMini width={50} color={color} />,
}, },
{ {
name: "trophie", name: "trophie",
icon: (color: string) => <TropfyIcon color={color} />, icon: (color: string) => <TropfyIcon color={color} />,
}, },
{ {
name: "flag", name: "flag",
icon: (color: string) => <FlagIcon color={color} />, icon: (color: string) => <FlagIcon color={color} />,
}, },
{ {
name: "heart", name: "heart",
icon: (color: string) => <HeartIcon color={color} />, icon: (color: string) => <HeartIcon color={color} />,
}, },
{ {
name: "like", name: "like",
icon: (color: string) => <LikeIcon color={color} />, icon: (color: string) => <LikeIcon color={color} />,
}, },
{ {
name: "bubble", name: "bubble",
icon: (color: string) => <LightbulbIcon color={color} />, icon: (color: string) => <LightbulbIcon color={color} />,
}, },
{ {
name: "hashtag", name: "hashtag",
icon: (color: string) => <HashtagIcon color={color} />, icon: (color: string) => <HashtagIcon color={color} />,
}, },
]; ];
export const Rating = ({ currentQuestion }: RatingProps) => { export const Rating = ({ currentQuestion }: RatingProps) => {
const { settings } = useQuestionsStore() const { settings } = useQuizData();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(650)); const isMobile = useMediaQuery(theme.breakpoints.down(650));
const { answer } = const { answer } =
answers.find( answers.find(
({ questionId }) => questionId === currentQuestion.id ({ questionId }) => questionId === currentQuestion.id
) ?? {}; ) ?? {};
const form = buttonRatingForm.find( const form = buttonRatingForm.find(
({ name }) => name === currentQuestion.content.form ({ name }) => name === currentQuestion.content.form
); );
if (!settings) throw new Error("settings is null"); return (
<Box>
return ( <Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
<Box> <Box
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography> sx={{
<Box display: "inline-flex",
sx={{ alignItems: "center",
display: "inline-flex", gap: "20px",
alignItems: "center", marginTop: "20px",
gap: "20px", flexDirection: "column",
marginTop: "20px", width: isMobile ? "100%" : undefined,
flexDirection: "column", }}
width: isMobile ? "100%" : undefined, >
}} <Box
> sx={{
<Box display: "inline-block",
sx={{ width: "100%",
display: "inline-block", }}
width: "100%", >
}} <RatingComponent
> value={Number(answer || 0)}
<RatingComponent onChange={async (_, value) => {
value={Number(answer || 0)}
onChange={async (_, value) => {
try { try {
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: settings.qid
}) });
updateAnswer(currentQuestion.id, String(value)) updateAnswer(currentQuestion.id, String(value));
} catch (e) { } catch (e) {
enqueueSnackbar("ответ не был засчитан") enqueueSnackbar("ответ не был засчитан");
} }
}} }}
sx={{ sx={{
height: "50px", height: "50px",
gap: isMobile ? undefined : "15px", gap: isMobile ? undefined : "15px",
justifyContent: isMobile ? "space-between" : undefined, justifyContent: isMobile ? "space-between" : undefined,
width: isMobile ? "100%" : undefined width: isMobile ? "100%" : undefined
}} }}
max={currentQuestion.content.steps} max={currentQuestion.content.steps}
icon={form?.icon(theme.palette.primary.main)} icon={form?.icon(theme.palette.primary.main)}
emptyIcon={form?.icon("#9A9AAF")} emptyIcon={form?.icon("#9A9AAF")}
/> />
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
gap: 2,
width: "100%",
}}
>
<Typography sx={{
color: "#9A9AAF"
// color: theme.palette.grey2.main
}}>
{currentQuestion.content.ratingNegativeDescription}
</Typography>
<Typography sx={{ color: "#9A9AAF" }}>
{currentQuestion.content.ratingPositiveDescription}
</Typography>
</Box>
</Box>
</Box> </Box>
<Box );
sx={{
display: "flex",
justifyContent: "space-between",
gap: 2,
width: "100%",
}}
>
<Typography sx={{
color: "#9A9AAF"
// color: theme.palette.grey2.main
}}>
{currentQuestion.content.ratingNegativeDescription}
</Typography>
<Typography sx={{ color: "#9A9AAF" }}>
{currentQuestion.content.ratingPositiveDescription}
</Typography>
</Box>
</Box>
</Box>
);
}; };

@ -7,75 +7,73 @@ import { useQuizViewStore, updateAnswer, deleteAnswer } from "@stores/quizView/s
import type { QuizQuestionSelect } from "../../../model/questionTypes/select"; import type { QuizQuestionSelect } from "../../../model/questionTypes/select";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { sendAnswer } from "@api/quizRelase"; import { sendAnswer } from "@api/quizRelase";
import { useQuizData } from "@utils/hooks/useQuizData";
import { useQuestionsStore } from "@stores/quizData/store"
type SelectProps = { type SelectProps = {
currentQuestion: QuizQuestionSelect; currentQuestion: QuizQuestionSelect;
}; };
export const Select = ({ currentQuestion }: SelectProps) => { export const Select = ({ currentQuestion }: SelectProps) => {
const theme = useTheme(); const theme = useTheme();
const { settings } = useQuestionsStore() const { settings } = useQuizData();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const { answer } = const { answer } =
answers.find( answers.find(
({ questionId }) => questionId === currentQuestion.id ({ questionId }) => questionId === currentQuestion.id
) ?? {}; ) ?? {};
if (!settings) throw new Error("settings is null"); return (
<Box>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
marginTop: "20px",
}}
>
<SelectComponent
placeholder={currentQuestion.content.default}
activeItemIndex={answer ? Number(answer) : -1}
items={currentQuestion.content.variants.map(({ answer }) => answer)}
colorMain={theme.palette.primary.main}
onChange={async (_, value) => {
if (value < 0) {
deleteAnswer(currentQuestion.id);
try {
return ( await sendAnswer({
<Box> questionId: currentQuestion.id,
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography> body: "",
<Box qid: settings.qid
sx={{ });
display: "flex",
flexDirection: "column",
width: "100%",
marginTop: "20px",
}}
>
<SelectComponent
placeholder={currentQuestion.content.default}
activeItemIndex={answer ? Number(answer) : -1}
items={currentQuestion.content.variants.map(({ answer }) => answer)}
colorMain={theme.palette.primary.main}
onChange={async(_, value) => {
if (value < 0) {
deleteAnswer(currentQuestion.id);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: settings.qid
})
} catch (e) {
enqueueSnackbar("ответ не был засчитан")
}
return;
}
try { } catch (e) {
enqueueSnackbar("ответ не был засчитан");
await sendAnswer({ }
questionId: currentQuestion.id, return;
body: String(currentQuestion.content.variants[Number(value)].answer), }
qid: settings.qid
})
updateAnswer(currentQuestion.id, String(value)); try {
} catch (e) { await sendAnswer({
enqueueSnackbar("ответ не был засчитан") questionId: currentQuestion.id,
} body: String(currentQuestion.content.variants[Number(value)].answer),
qid: settings.qid
});
updateAnswer(currentQuestion.id, String(value));
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
}} }}
/> />
</Box> </Box>
</Box> </Box>
); );
}; };

@ -6,63 +6,61 @@ import { useQuizViewStore, updateAnswer } from "@stores/quizView/store";
import type { QuizQuestionText } from "../../../model/questionTypes/text"; import type { QuizQuestionText } from "../../../model/questionTypes/text";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useQuestionsStore } from "@stores/quizData/store"
import { sendAnswer } from "@api/quizRelase"; import { sendAnswer } from "@api/quizRelase";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import { useQuizData } from "@utils/hooks/useQuizData";
type TextProps = { type TextProps = {
currentQuestion: QuizQuestionText; currentQuestion: QuizQuestionText;
}; };
export const Text = ({ currentQuestion }: TextProps) => { export const Text = ({ currentQuestion }: TextProps) => {
const theme = useTheme(); const theme = useTheme();
const { settings } = useQuestionsStore() const { settings } = useQuizData();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {}; const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const inputHC = useDebouncedCallback(async (text) => { const inputHC = useDebouncedCallback(async (text) => {
if (!settings) return; try {
try {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: text, body: text,
qid: settings.qid qid: settings.qid
}) });
} catch (e) { } catch (e) {
enqueueSnackbar("ответ не был засчитан") enqueueSnackbar("ответ не был засчитан");
} }
}, 400); }, 400);
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography> <Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
width: "100%", width: "100%",
marginTop: "20px", marginTop: "20px",
}} }}
> >
<CustomTextField <CustomTextField
placeholder={currentQuestion.content.placeholder} placeholder={currentQuestion.content.placeholder}
//@ts-ignore //@ts-ignore
value={answer || ""} value={answer || ""}
onChange={async ({ target }) => { onChange={async ({ target }) => {
updateAnswer(currentQuestion.id, target.value) updateAnswer(currentQuestion.id, target.value);
inputHC(target.value) inputHC(target.value);
} }
} }
sx={{ sx={{
"&:focus-visible": { "&:focus-visible": {
borderColor: theme.palette.primary.main borderColor: theme.palette.primary.main
} }
}} }}
/> />
</Box> </Box>
</Box> </Box>
); );
}; };

@ -29,7 +29,7 @@ import { quizThemes } from "@utils/themes/Publication/themePublication";
import { enqueueSnackbar } from "notistack"; 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 { useQuestionsStore } from "@stores/quizData/store"; import { useQuizData } from "@utils/hooks/useQuizData";
const TextField = MuiTextField as unknown as FC<TextFieldProps>; const TextField = MuiTextField as unknown as FC<TextFieldProps>;
@ -135,11 +135,9 @@ const VariantItem = ({
index, index,
own = false, own = false,
}: VariantItemProps) => { }: VariantItemProps) => {
const { settings } = useQuestionsStore() const { settings } = useQuizData();
const theme = useTheme(); const theme = useTheme();
if (!settings) throw new Error("settings is null");
return ( return (
<FormControlLabel <FormControlLabel
key={variant.id} key={variant.id}
@ -195,7 +193,7 @@ const VariantItem = ({
? currentAnswer?.filter((item) => item !== variantId) ? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId], : [...currentAnswer, variantId],
qid: settings.qid qid: settings.qid
}) });
updateAnswer( updateAnswer(
currentQuestion.id, currentQuestion.id,
@ -205,7 +203,7 @@ const VariantItem = ({
); );
} catch (e) { } catch (e) {
enqueueSnackbar("ответ не был засчитан") enqueueSnackbar("ответ не был засчитан");
} }
@ -218,12 +216,12 @@ const VariantItem = ({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: currentQuestion.content.variants[index].answer, body: currentQuestion.content.variants[index].answer,
qid: settings.qid qid: settings.qid
}) });
updateAnswer(currentQuestion.id, variantId); updateAnswer(currentQuestion.id, variantId);
} catch (e) { } catch (e) {
enqueueSnackbar("ответ не был засчитан") enqueueSnackbar("ответ не был засчитан");
} }
if (answer === variantId) { if (answer === variantId) {
@ -233,10 +231,10 @@ const VariantItem = ({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: "", body: "",
qid: settings.qid qid: settings.qid
}) });
} catch (e) { } catch (e) {
enqueueSnackbar("ответ не был засчитан") enqueueSnackbar("ответ не был засчитан");
} }
deleteAnswer(currentQuestion.id); deleteAnswer(currentQuestion.id);
} }

@ -1,16 +1,12 @@
import { import {
Box, Box,
Typography, Typography,
RadioGroup, RadioGroup,
FormControlLabel, FormControlLabel,
Radio, Radio,
useTheme, useTheme,
useMediaQuery useMediaQuery
} from "@mui/material"; } from "@mui/material";
import gag from "./gag.png"
import { useQuestionsStore } from "@stores/quizData/store"
import { useQuizViewStore, updateAnswer, deleteAnswer } from "@stores/quizView/store"; import { useQuizViewStore, updateAnswer, deleteAnswer } from "@stores/quizView/store";
import RadioCheck from "@ui_kit/RadioCheck"; import RadioCheck from "@ui_kit/RadioCheck";
@ -20,147 +16,151 @@ import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { sendAnswer } from "@api/quizRelase"; import { sendAnswer } from "@api/quizRelase";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import BlankImage from "@icons/BlankImage";
import { useQuizData } from "@utils/hooks/useQuizData";
type VarimgProps = { type VarimgProps = {
currentQuestion: QuizQuestionVarImg; currentQuestion: QuizQuestionVarImg;
}; };
export const Varimg = ({ currentQuestion }: VarimgProps) => { export const Varimg = ({ currentQuestion }: VarimgProps) => {
const { settings } = useQuestionsStore() const { settings } = useQuizData();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(650)); const isMobile = useMediaQuery(theme.breakpoints.down(650));
const { answer } = const { answer } =
answers.find( answers.find(
({ questionId }) => questionId === currentQuestion.id ({ questionId }) => questionId === currentQuestion.id
) ?? {}; ) ?? {};
const variant = currentQuestion.content.variants.find( const variant = currentQuestion.content.variants.find(
({ id }) => answer === id ({ id }) => answer === id
); );
if (!settings) throw new Error("settings is null"); return (
<Box>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
<Box sx={{
display: "flex",
marginTop: "20px",
flexDirection: isMobile ? "column-reverse" : undefined,
gap: isMobile ? "30px" : undefined
return ( }}>
<Box> <RadioGroup
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography> name={currentQuestion.id}
<Box sx={{ value={currentQuestion.content.variants.findIndex(
display: "flex", ({ id }) => answer === id
marginTop: "20px", )}
flexDirection: isMobile ? "column-reverse" : undefined, sx={{
gap: isMobile ? "30px" : undefined display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
flexBasis: "100%",
}}
>
<Box sx={{ display: "flex", flexDirection: "column", width: "100%", gap: isMobile ? "20px" : undefined }}>
{currentQuestion.content.variants.map((variant, index) => (
<FormControlLabel
key={variant.id}
sx={{
marginBottom: "15px",
borderRadius: "5px",
padding: "15px",
color: theme.palette.text.primary,
backgroundColor: quizThemes[settings.cfg.theme].isLight ? "white" : theme.palette.background.default,
border: `1px solid`,
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
display: "flex",
margin: isMobile ? 0 : undefined,
"& .MuiFormControlLabel-label": {
wordBreak: "break-word"
}
}}
value={index}
onClick={async (event) => {
event.preventDefault();
}}>
<RadioGroup
name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex(
({ id }) => answer === id
)}
sx={{
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
flexBasis: "100%",
}}
>
<Box sx={{ display: "flex", flexDirection: "column", width: "100%", gap: isMobile ? "20px" : undefined }}>
{currentQuestion.content.variants.map((variant, index) => (
<FormControlLabel
key={variant.id}
sx={{
marginBottom: "15px",
borderRadius: "5px",
padding: "15px",
color: theme.palette.text.primary,
backgroundColor: quizThemes[settings.cfg.theme].isLight ? "white" : theme.palette.background.default,
border: `1px solid`,
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
display: "flex",
margin: isMobile ? 0 : undefined,
"& .MuiFormControlLabel-label": {
wordBreak: "break-word"
}
}}
value={index}
onClick={async(event) => {
event.preventDefault();
try {
await sendAnswer({ try {
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}"/>`,
qid: settings.qid
})
updateAnswer(
currentQuestion.id,
currentQuestion.content.variants[index].id
);
} catch (e) { await sendAnswer({
enqueueSnackbar("ответ не был засчитан") 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}"/>`,
qid: settings.qid
});
updateAnswer(
if (answer === currentQuestion.content.variants[index].id) { currentQuestion.id,
try { currentQuestion.content.variants[index].id
);
await sendAnswer({
questionId: currentQuestion.id, } catch (e) {
body: "", enqueueSnackbar("ответ не был засчитан");
qid: settings.qid }
})
} catch (e) { if (answer === currentQuestion.content.variants[index].id) {
enqueueSnackbar("ответ не был засчитан") try {
}
deleteAnswer(currentQuestion.id); await sendAnswer({
} questionId: currentQuestion.id,
}} body: "",
control={ qid: settings.qid
<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main}/>} icon={<RadioIcon />} /> });
}
label={variant.answer} } catch (e) {
/> enqueueSnackbar("ответ не был засчитан");
))} }
</Box> deleteAnswer(currentQuestion.id);
</RadioGroup> }
{/* {(variant?.extendedText || currentQuestion.content.back) && ( */} }}
<Box control={
sx={{ <Radio checkedIcon={<RadioCheck color={theme.palette.primary.main} />} icon={<RadioIcon />} />
maxWidth: "450px", }
width: "100%", label={variant.answer}
height: "450px", />
border: "1px solid #9A9AAF", ))}
borderRadius: "12px", </Box>
overflow: "hidden", </RadioGroup>
display: "flex", {/* {(variant?.extendedText || currentQuestion.content.back) && ( */}
alignItems: "center", <Box
justifyContent: "center", sx={{
backgroundColor: "#9A9AAF12", maxWidth: "450px",
color: "#9A9AAF" width: "100%",
}} height: "450px",
> border: "1px solid #9A9AAF",
{answer ? ( borderRadius: "12px",
<img overflow: "hidden",
src={variant?.extendedText || gag} display: "flex",
style={{ width: "100%", height: "100%", objectFit: "cover" }} alignItems: "center",
alt="" justifyContent: "center",
/> backgroundColor: "#9A9AAF12",
) : (currentQuestion.content.replText !== " " && currentQuestion.content.replText.length > 0) ? currentQuestion.content.replText : variant?.extendedText || isMobile ? ( color: "#9A9AAF"
"Выберите вариант ответа ниже" }}
) : ( >
"Выберите вариант ответа слева" {answer ? (
)} variant?.extendedText ? (
<img
</Box> src={variant?.extendedText}
{/* )} */} style={{ width: "100%", height: "100%", objectFit: "cover" }}
</Box> alt=""
</Box> />
); ) : (
<BlankImage />
)
) : (currentQuestion.content.replText !== " " && currentQuestion.content.replText.length > 0) ? currentQuestion.content.replText : variant?.extendedText || isMobile ? (
"Выберите вариант ответа ниже"
) : (
"Выберите вариант ответа слева"
)}
</Box>
{/* )} */}
</Box>
</Box>
);
}; };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

@ -2,62 +2,62 @@ import { ChangeEvent, useEffect, useRef, useState } from "react";
import { Box, Button, Typography } from "@mui/material"; import { Box, Button, Typography } from "@mui/material";
import type { import type {
QuizQuestionFile, QuizQuestionFile,
UploadFileType, UploadFileType,
} from "model/questionTypes/file"; } from "@model/questionTypes/file";
export const UPLOAD_FILE_TYPES_MAP: Record<UploadFileType, string> = { const UPLOAD_FILE_TYPES_MAP: Record<UploadFileType, string> = {
picture: "image/*", picture: "image/*",
video: "video/*", video: "video/*",
audio: "audio/*", audio: "audio/*",
document: document:
".doc,.docx,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,.pdf", ".doc,.docx,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,.pdf",
} as const; } as const;
interface Props { interface Props {
question: QuizQuestionFile; question: QuizQuestionFile;
} }
export default function File({ question }: Props) { export default function File({ question }: Props) {
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [acceptedType, setAcceptedType] = useState<any>( const [acceptedType, setAcceptedType] = useState<any>(
UPLOAD_FILE_TYPES_MAP.picture UPLOAD_FILE_TYPES_MAP.picture
); );
useEffect(() => { useEffect(() => {
setAcceptedType(UPLOAD_FILE_TYPES_MAP[question.content.type]); setAcceptedType(UPLOAD_FILE_TYPES_MAP[question.content.type]);
}, [question.content.type]); }, [question.content.type]);
function handleFileChange(event: ChangeEvent<HTMLInputElement>) { function handleFileChange(event: ChangeEvent<HTMLInputElement>) {
if (!event.target.files?.[0]) return setFile(null); if (!event.target.files?.[0]) return setFile(null);
setFile(event.target.files[0]); setFile(event.target.files[0]);
} }
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "start", alignItems: "start",
gap: 1, gap: 1,
}} }}
> >
<Typography variant="h6" data-cy="question-title">{question.title}</Typography> <Typography variant="h6" data-cy="question-title">{question.title}</Typography>
<Button variant="contained" onClick={() => fileInputRef.current?.click()}> <Button variant="contained" onClick={() => fileInputRef.current?.click()}>
Загрузить файл Загрузить файл
<input <input
ref={fileInputRef} ref={fileInputRef}
onChange={handleFileChange} onChange={handleFileChange}
type="file" type="file"
accept={acceptedType} accept={acceptedType}
data-cy="file-upload-input" data-cy="file-upload-input"
style={{ style={{
display: "none", display: "none",
}} }}
/> />
</Button> </Button>
{file && <Typography data-cy="chosen-file-name">Выбран файл: {file.name}</Typography>} {file && <Typography data-cy="chosen-file-name">Выбран файл: {file.name}</Typography>}
</Box> </Box>
); );
} }

@ -1,12 +0,0 @@
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { useQuestionsStore } from "./store";
import { QuizSettings } from "@model/settingsData";
export const getQuestionById = (questionId: string | null): AnyTypedQuizQuestion | null => {
if (questionId === null) return null;
return useQuestionsStore.getState().items.find(q => q.id === questionId || q.content.id === questionId) || null;
};
export const setQuizData = (quizData: QuizSettings) => useQuestionsStore.setState(quizData);

@ -1,29 +0,0 @@
import { QuizSettings } from "@model/settingsData";
import { create } from "zustand";
import { devtools } from "zustand/middleware";
type QuizDataStore = {
settings: QuizSettings["settings"] | null;
items: QuizSettings["items"];
cnt: QuizSettings["cnt"];
recentlyСompleted: boolean;
};
const initialState: QuizDataStore = {
settings: null,
items: [],
cnt: 0,
recentlyСompleted: false,
};
export const useQuestionsStore = create<QuizDataStore>()(
devtools(
() => initialState,
{
name: "QuizDataStore",
enabled: import.meta.env.DEV,
trace: import.meta.env.DEV,
}
)
);

@ -0,0 +1,43 @@
import { ErrorInfo } from "react";
interface ComponentError {
timestamp: number;
message: string;
callStack: string | undefined;
componentStack: string | null | undefined;
}
export function handleComponentError(error: Error, info: ErrorInfo) {
const componentError: ComponentError = {
timestamp: Math.floor(Date.now() / 1000),
message: error.message,
callStack: error.stack,
componentStack: info.componentStack,
};
queueErrorRequest(componentError);
}
let errorsQueue: ComponentError[] = [];
let timeoutId: ReturnType<typeof setTimeout>;
function queueErrorRequest(error: ComponentError) {
errorsQueue.push(error);
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
sendErrorsToServer();
}, 1000);
}
async function sendErrorsToServer() {
// makeRequest({
// url: "",
// method: "POST",
// body: errorsQueue,
// useToken: true,
// });
console.log(`Fake-sending ${errorsQueue.length} errors to server`, errorsQueue);
errorsQueue = [];
}

@ -1,56 +0,0 @@
import { useEffect, useLayoutEffect, useRef, useState } from "react"
import { useQuestionsStore } from "@stores/quizData/store";
import { getData } from "@api/quizRelase"
interface SettingsGetter {
quizId: string
}
export function useGetSettings(quizId: string) {
const [fetchState, setFetchState] = useState<"fetching" | "idle" | "all fetched">("idle")
useEffect(() => {
async function get() {
const data = await getData(quizId)
//@ts-ignore
const settings = data.settings
const parseData = {
settings: {
fp: settings.fp,
rep: settings.rep,
name: settings.name,
cfg: JSON.parse(settings?.cfg),
lim: settings.lim,
due: settings.due,
delay: settings.delay,
pausable: settings.pausable
},
//@ts-ignore
items: data.items.map((item) => {
const content = JSON.parse(item.c)
return {
description: item.desc,
id: item.id,
page: item.p,
required: item.req,
title: item.title,
type: item.typ,
content
}
}),
//@ts-ignore
cnt: data.cnt
}
//@ts-ignore
useQuestionsStore.setState(parseData)
}
get()
// const controller = new AbortController()
}, [])
return
// return
}

@ -0,0 +1,34 @@
import { getData } from "@api/quizRelase";
import { parseQuizData } from "@model/api/getQuizData";
import { QuizSettings } from "@model/settingsData";
import { enqueueSnackbar } from "notistack";
import useSWR from "swr";
import { useQuizId } from "../../contexts/QuizIdContext";
import { replaceSpacesToEmptyLines } from "../../pages/ViewPublicationPage/tools/replaceSpacesToEmptyLines";
export function useQuizData() {
const quizId = useQuizId();
const { data } = useSWR(["quizData", quizId], params => getQuizData(params[1]), {
suspense: true,
});
return data;
}
async function getQuizData(quizId: string) {
const response = await getData(quizId);
const quizDataResponse = response.data;
if (response.error) {
enqueueSnackbar(response.error);
throw new Error(response.error);
}
if (!quizDataResponse) {
throw new Error("Quiz not found");
}
const quizSettings = replaceSpacesToEmptyLines(parseQuizData(quizDataResponse, quizId));
return JSON.parse(JSON.stringify({ data: quizSettings }).replaceAll(/\\" \\"/g, '""').replaceAll(/" "/g, '""')).data as QuizSettings;
}

@ -1,5 +1,5 @@
import WidgetApp from "./WidgetApp";
import { Root, createRoot } from "react-dom/client"; import { Root, createRoot } from "react-dom/client";
import App from "./App";
let root: Root | undefined = undefined; let root: Root | undefined = undefined;
@ -14,7 +14,7 @@ const widget = {
root = createRoot(element); root = createRoot(element);
root.render(<App widget quizId={quizId} />); root.render(<WidgetApp quizId={quizId} />);
}, },
unmount() { unmount() {
if (root) root.unmount(); if (root) root.unmount();

@ -19,25 +19,24 @@
/* Linting */ /* Linting */
"strict": true, "strict": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"baseUrl": "./src",
"paths": { "paths": {
"@ui_kit/*": [ "@ui_kit/*": [
"./ui_kit/*" "./src/ui_kit/*"
], ],
"@icons/*": [ "@icons/*": [
"./assets/icons/*" "./src/assets/icons/*"
], ],
"@stores/*": [ "@stores/*": [
"./stores/*" "./src/stores/*"
], ],
"@api/*": [ "@api/*": [
"./api/*" "./src/api/*"
], ],
"@model/*": [ "@model/*": [
"./model/*" "./src/model/*"
], ],
"@utils/*": [ "@utils/*": [
"./utils/*" "./src/utils/*"
] ]
} }
}, },

@ -737,6 +737,11 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
"@remix-run/router@1.14.2":
version "1.14.2"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.14.2.tgz#4d58f59908d9197ba3179310077f25c88e49ed17"
integrity sha512-ACXpdMM9hmKZww21yEqWwiLws/UPLhNKvimN8RrYSqPSvB3ov7sLvAcfvaxePeLvccTQKGdkDIhLYApZVDFuKg==
"@rollup/rollup-android-arm-eabi@4.9.5": "@rollup/rollup-android-arm-eabi@4.9.5":
version "4.9.5" version "4.9.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.5.tgz#b752b6c88a14ccfcbdf3f48c577ccc3a7f0e66b9" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.5.tgz#b752b6c88a14ccfcbdf3f48c577ccc3a7f0e66b9"
@ -2709,6 +2714,13 @@ react-dom@^18.2.0:
loose-envify "^1.1.0" loose-envify "^1.1.0"
scheduler "^0.23.0" scheduler "^0.23.0"
react-error-boundary@^4.0.12:
version "4.0.12"
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.0.12.tgz#59f8f1dbc53bbbb34fc384c8db7cf4082cb63e2c"
integrity sha512-kJdxdEYlb7CPC1A0SeUY38cHpjuu6UkvzKiAmqmOFL21VRfMhOcWxTCBgLVCO0VEMh9JhFNcVaXlV4/BTpiwOA==
dependencies:
"@babel/runtime" "^7.12.5"
react-is@^16.13.1, react-is@^16.7.0: react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@ -2724,6 +2736,21 @@ react-refresh@^0.14.0:
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e"
integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ== integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==
react-router-dom@^6.21.3:
version "6.21.3"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.21.3.tgz#ef3a7956a3699c7b82c21fcb3dbc63c313ed8c5d"
integrity sha512-kNzubk7n4YHSrErzjLK72j0B5i969GsuCGazRl3G6j1zqZBLjuSlYBdVdkDOgzGdPIffUOc9nmgiadTEVoq91g==
dependencies:
"@remix-run/router" "1.14.2"
react-router "6.21.3"
react-router@6.21.3:
version "6.21.3"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.21.3.tgz#8086cea922c2bfebbb49c6594967418f1f167d70"
integrity sha512-a0H638ZXULv1OdkmiK6s6itNhoy33ywxmUFT/xtSoVyf9VnC7n7+VT4LjVzdIHSaF5TIh9ylUgxMXksHTgGrKg==
dependencies:
"@remix-run/router" "1.14.2"
react-transition-group@^4.4.5: react-transition-group@^4.4.5:
version "4.4.5" version "4.4.5"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"