Merge branch 'dev' of penahub.gitlab.yandexcloud.net:frontend/squiz into view-answers-fixes

This commit is contained in:
IlyaDoronin 2023-12-14 15:22:16 +03:00
commit 5987a20843
58 changed files with 1297 additions and 1001 deletions

@ -35,7 +35,6 @@
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-cytoscapejs": "^2.0.0",
"react-datepicker": "^4.24.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
@ -78,7 +77,6 @@
"@types/cytoscape-popper": "^2.0.4",
"@types/react-beautiful-dnd": "^13.1.4",
"@types/react-cytoscapejs": "^1.2.4",
"@types/react-datepicker": "^4.19.3",
"craco-alias": "^3.0.1",
"cypress": "^13.4.0"
}

@ -5,7 +5,7 @@ import "dayjs/locale/ru";
import SigninDialog from "./pages/auth/Signin";
import SignupDialog from "./pages/auth/Signup";
import { ViewPage } from "./pages/ViewPublicationPage";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { BrowserRouter, Route, Routes, useLocation, useNavigate, Navigate } from "react-router-dom";
import "./index.css";
import ContactFormPage from "./pages/ContactFormPage/ContactFormPage";
import InstallQuiz from "./pages/InstallQuiz/InstallQuiz";
@ -34,6 +34,8 @@ const routeslink = [
export default function App() {
const userId = useUserStore((state) => state.userId);
const location = useLocation()
const navigate = useNavigate()
useUserFetcher({
url: `https://hub.pena.digital/user/${userId}`,
@ -48,23 +50,31 @@ export default function App() {
}
},
});
if (location.state?.redirectTo)
return <Navigate to={location.state.redirectTo} replace state={{ backgroundLocation: location }} />
return (
<>
<ContactFormModal />
<BrowserRouter>
{location.state?.backgroundLocation && (
<Routes>
{routeslink.map((e, i) => (
<Route key={i} path={e.path} element={<Main page={e.page} header={e.header} sidebar={e.sidebar} />} />
))}
<Route path="edit" element={<EditPage />} />
<Route path="crop" element={<ImageCrop />} />
<Route path="/" element={<Landing />} />
<Route path="/signin" element={<SigninDialog />} />
<Route path="/signup" element={<SignupDialog />} />
<Route path="/view" element={<ViewPage />} />
</Routes>
</BrowserRouter>
)}
<Routes location={location.state?.backgroundLocation || location}>
{routeslink.map((e, i) => (
<Route key={i} path={e.path} element={<Main page={e.page} header={e.header} sidebar={e.sidebar} />} />
))}
<Route path="edit" element={<EditPage />} />
<Route path="crop" element={<ImageCrop />} />
<Route path="/" element={<Landing />} />
<Route path="/signin" element={<Navigate to="/" replace state={{ redirectTo: "/signin" }} />} />
<Route path="/signup" element={<Navigate to="/" replace state={{ redirectTo: "/signup" }} />} />/>
<Route path="/view" element={<ViewPage />} />
</Routes>
</>
);
}

@ -18,6 +18,7 @@ function createQuestion(body: CreateQuestionRequest) {
}
async function getQuestionList(body?: Partial<GetQuestionListRequest>) {
console.log("body" , body)
if (!body?.quiz_id) return null;
const response = await makeRequest<GetQuestionListRequest, GetQuestionListResponse>({

@ -0,0 +1,43 @@
import { Box, useTheme } from "@mui/material";
type CheckboxIconProps = {
checked?: boolean;
};
export const CheckboxIcon = ({ checked = false }: CheckboxIconProps) => {
const theme = useTheme();
return (
<Box
sx={{
height: "24px",
width: "24px",
borderRadius: "6px",
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: checked
? theme.palette.brightPurple.main
: theme.palette.background.default,
border: `1px solid ${theme.palette.grey2.main}`,
}}
>
{checked && (
<svg
xmlns="http://www.w3.org/2000/svg"
width="15"
height="15"
viewBox="0 0 25 18"
fill="none"
>
<path
d="M2 9L10 16.5L22.5 1.5"
stroke="#ffffff"
strokeWidth="4"
strokeLinecap="round"
/>
</svg>
)}
</Box>
);
};

@ -12,6 +12,8 @@ import { createRoot } from "react-dom/client";
import "./index.css";
import lightTheme from "./utils/themes/light";
import { SWRConfig } from "swr";
import {BrowserRouter} from "react-router-dom";
dayjs.locale("ru");
@ -28,13 +30,16 @@ root.render(
<DndProvider backend={HTML5Backend}>
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ru" localeText={localeText}>
<ThemeProvider theme={lightTheme}>
<SnackbarProvider
preventDuplicate={true}
style={{ backgroundColor: lightTheme.palette.brightPurple.main }}
>
<CssBaseline />
<App />
</SnackbarProvider>
<BrowserRouter>
<SnackbarProvider
preventDuplicate={true}
style={{ backgroundColor: lightTheme.palette.brightPurple.main }}
>
<CssBaseline />
<App />
</SnackbarProvider>
</BrowserRouter>
</ThemeProvider>
</LocalizationProvider>
</DndProvider>

@ -60,7 +60,6 @@ export interface QuizQuestionBase {
type?: QuestionType | null;
expanded: boolean;
openedModalSettings: boolean;
required: boolean;
deleted: boolean;
deleteTimeoutId: number;
content: {

@ -7,7 +7,7 @@ import QuizLogo from "./images/icons/QuizLogo";
import { useMediaQuery, useTheme } from "@mui/material";
import { setIsContactFormOpen } from "../../stores/contactForm";
import { useUserStore } from "@root/user";
import { useNavigate } from "react-router-dom";
import { useNavigate, Link, useLocation } from "react-router-dom";
const buttonMenu = ["Меню 1", "Меню 2", "Меню 3", "Меню 4", "Меню 5", "Меню 1", "Меню 2"];
@ -18,8 +18,9 @@ export default function Component() {
const [select, setSelect] = React.useState(0);
const userId = useUserStore((state) => state.userId);
const navigate = useNavigate();
const location = useLocation()
const onClick = () => (userId ? navigate("/list") : setIsContactFormOpen(true));
const onClick = () => (userId ? navigate("/list") : navigate("/signin"));
return (
<SectionStyled
@ -66,7 +67,7 @@ export default function Component() {
{/* ))}*/}
{/*</Box>*/}
<Button
variant="outlined"
variant="outlined"
onClick={onClick}
sx={{
color: "black",

@ -9,7 +9,7 @@ import Blog from './Blog';
import HowItWorks from './HowItWorks';
import BusinessPluses from './BusinessPluses';
import HowToUse from './HowToUse';
import WhatTheySay from './WhatTheySay';
import StartWithTemplates from './StartWithTemplates';
import WhatTheFeatures from './WhatTheFeatures';
import FullScreenDialog from "./headerMobileLanding";
@ -18,13 +18,14 @@ import Collaboration from "./Collaboration";
export default function Landing() {
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
return (
<>
<CssBaseline />
<Header/>
<Hero/>
<Counter/>
<Collaboration/>
{/* <Collaboration/> */}
<HowItWorks/>
<BusinessPluses/>
<HowToUse/>

@ -7,7 +7,8 @@ import { useCurrentQuiz } from "@root/quizes/hooks";
import { updateRootContentId } from "@root/quizes/actions"
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"
import { useQuestionsStore } from "@root/questions/store";
import { deleteQuestion, cleardragQuestionContentId, updateQuestion, updateOpenedModalSettingsId, getQuestionByContentId, clearRuleForAll } from "@root/questions/actions";
import { deleteQuestion, updateQuestion, updateOpenedModalSettingsId, getQuestionByContentId, clearRuleForAll } from "@root/questions/actions";
import { cleardragQuestionContentId } from "@root/uiTools/actions";
import { withErrorBoundary } from "react-error-boundary";
import { storeToNodes } from "./helper";
@ -22,6 +23,7 @@ import type {
ElementDefinition,
} from "cytoscape";
import { enqueueSnackbar } from "notistack";
import { useUiTools } from "@root/uiTools/store";
type PopperItem = {
id: () => string;
@ -121,7 +123,7 @@ function CsComponent({
}: Props) {
const quiz = useCurrentQuiz();
const { dragQuestionContentId, desireToOpenABranchingModal } = useQuestionsStore()
const { dragQuestionContentId, desireToOpenABranchingModal } = useUiTools()
const trashQuestions = useQuestionsStore().questions
const questions = trashQuestions.filter((question) => question.type !== "result" && question.type !== null)
const [startCreate, setStartCreate] = useState("");
@ -145,8 +147,8 @@ function CsComponent({
}, [desireToOpenABranchingModal])
useLayoutEffect(() => {
updateOpenedModalSettingsId()
// updateRootContentId(quiz.id, "")
// clearRuleForAll()
// updateRootContentId(quiz.id, "")
// clearRuleForAll()
}, [])
useEffect(() => {
if (modalQuestionTargetContentId.length !== 0 && modalQuestionParentContentId.length !== 0) {
@ -157,6 +159,12 @@ function CsComponent({
}, [modalQuestionTargetContentId])
const addNode = ({ parentNodeContentId, targetNodeContentId }: { parentNodeContentId: string, targetNodeContentId?: string }) => {
//запрещаем работу родителя-ребенка если это один и тот же вопрос
if (parentNodeContentId === targetNodeContentId) return
const cy = cyRef?.current
const parentNodeChildren = cy?.$('edge[source = "' + parentNodeContentId + '"]')?.length
//если есть инфо о выбранном вопросе из модалки - берём родителя из инфо модалки. Иначе из значения дропа
@ -218,6 +226,7 @@ function CsComponent({
const removeNode = ({ targetNodeContentId }: { targetNodeContentId: string }) => {
console.log("старт удаление")
const deleteNodes = [] as string[]
const deleteEdges: any = []
const cy = cyRef?.current
@ -304,7 +313,6 @@ function CsComponent({
const clearDataAfterRemoveNode = ({ targetQuestionContentId, parentQuestionContentId }: { targetQuestionContentId: string, parentQuestionContentId: string }) => {
console.log("target ", targetQuestionContentId, "parent ", parentQuestionContentId)
updateQuestion(targetQuestionContentId, question => {
@ -317,7 +325,6 @@ function CsComponent({
//чистим rule родителя
const parentQuestion = getQuestionByContentId(parentQuestionContentId)
console.log(parentQuestion.content.rule.parentId)
const newRule = {}
const newChildren = [...parentQuestion.content.rule.children]
newChildren.splice(parentQuestion.content.rule.children.indexOf(targetQuestionContentId), 1);
@ -631,7 +638,6 @@ function CsComponent({
},
});
let gearsPopper = null
console.log('POPE', node.data())
if (node.data().root !== true) {
gearsPopper = node.popper({
popper: {
@ -639,7 +645,6 @@ console.log('POPE', node.data())
modifiers: [{ name: "flip", options: { boundary: node } }],
},
content: ([item]) => {
console.log('PEPO', item.id())
const itemId = item.id();
const itemElement = gearsContainer.current?.querySelector(
@ -670,7 +675,6 @@ console.log('POPE', node.data())
};
const onZoom = (event: AbstractEventObject) => {
console.log('ZOOOOM')
const zoom = event.cy.zoom();
//update();

@ -1,19 +1,30 @@
import { Box } from "@mui/material"
import { useEffect, useRef, useState } from "react";
import { deleteQuestion, updateDragQuestionContentId, updateQuestion } from "@root/questions/actions"
import { useEffect, useRef, useLayoutEffect } from "react";
import { deleteQuestion, clearRuleForAll, updateQuestion, updateOpenedModalSettingsId } from "@root/questions/actions"
import { updateRootContentId } from "@root/quizes/actions"
import { useCurrentQuiz } from "@root/quizes/hooks"
import { useQuestionsStore } from "@root/questions/store"
import { enqueueSnackbar } from "notistack";
import { useUiTools } from "@root/uiTools/store";
interface Props {
setOpenedModalQuestions: (open: boolean) => void;
modalQuestionTargetContentId: string;
}
export const FirstNodeField = ({ setOpenedModalQuestions, modalQuestionTargetContentId }: Props) => {
const quiz = useCurrentQuiz();
const { dragQuestionContentId, questions } = useQuestionsStore()
useLayoutEffect(() => {
updateOpenedModalSettingsId()
console.log("first render firstComponent")
// updateRootContentId(quiz.id, "")
// clearRuleForAll()
}, [])
const { questions } = useQuestionsStore()
const { dragQuestionContentId } = useUiTools()
const Container = useRef<HTMLDivElement | null>(null);
const modalOpen = () => setOpenedModalQuestions(true)

@ -1,15 +1,15 @@
import { Box } from "@mui/material";
import { FirstNodeField } from "./FirstNodeField";
import CsComponent from "./CsComponent";
import { useQuestionsStore } from "@root/questions/store"
import { useCurrentQuiz } from "@root/quizes/hooks";
import { useState } from "react";
import {BranchingQuestionsModal} from "../BranchingQuestionsModal"
import { useUiTools } from "@root/uiTools/store";
export const BranchingMap = () => {
const quiz = useCurrentQuiz();
const { dragQuestionContentId } = useQuestionsStore()
const { dragQuestionContentId } = useUiTools()
const [modalQuestionParentContentId, setModalQuestionParentContentId] = useState<string>("")
const [modalQuestionTargetContentId, setModalQuestionTargetContentId] = useState<string>("")
const [openedModalQuestions, setOpenedModalQuestions] = useState<boolean>(false)

@ -1,15 +1,16 @@
import {Box, Typography, Switch, useTheme, Button, useMediaQuery, SxProps, Theme} from "@mui/material";
import { QuestionsList } from "./QuestionsList";
import { updateOpenBranchingPanel } from "@root/questions/actions";
import { updateOpenBranchingPanel } from "@root/uiTools/actions";
import {useQuestionsStore} from "@root/questions/store";
import {useRef} from "react";
import { useUiTools } from "@root/uiTools/store";
export const BranchingPanel = (sx?: SxProps<Theme>) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(660));
const {openBranchingPanel} = useQuestionsStore.getState()
const {openBranchingPanel} = useUiTools()
const ref = useRef()
return (
<Box sx={{ userSelect: "none", maxWidth: "350px", width: "100%" }}>
@ -26,10 +27,10 @@ export const BranchingPanel = (sx?: SxProps<Theme>) => {
}}
>
<Switch
value={openBranchingPanel}
onChange={(_, value) => {
updateOpenBranchingPanel(value)
}}
checked={openBranchingPanel}
onChange={
(e) => updateOpenBranchingPanel(e.target.checked)
}
sx={{
width: 50,
height: 30,
@ -76,7 +77,7 @@ export const BranchingPanel = (sx?: SxProps<Theme>) => {
/>
<Box>
<Typography ref={ref} sx={{ fontWeight: "bold", color: "#4D4D4D" }}>
Логика ветвления
Логика ветвления
</Typography>
<Typography sx={{ color: "#4D4D4D", fontSize: "12px" }}>
Настройте связи между вопросами

@ -11,7 +11,8 @@ import {
useMediaQuery,
useTheme,
} from "@mui/material";
import { copyQuestion, deleteQuestion, updateOpenBranchingPanel, updateDesireToOpenABranchingModal, deleteQuestionWithTimeout } from "@root/questions/actions";
import { copyQuestion, deleteQuestion, deleteQuestionWithTimeout, clearRuleForAll, updateQuestion, getQuestionByContentId } from "@root/questions/actions";
import { updateOpenBranchingPanel, updateDesireToOpenABranchingModal, } from "@root/uiTools/actions";
import MiniButtonSetting from "@ui_kit/MiniButtonSetting";
import { CopyIcon } from "../../assets/icons/questionsPage/CopyIcon";
import Branching from "../../assets/icons/questionsPage/branching";
@ -23,6 +24,8 @@ import { useCurrentQuiz } from "@root/quizes/hooks";
import { enqueueSnackbar } from "notistack";
import { useQuestionsStore } from "@root/questions/store";
import { updateOpenedModalSettingsId } from "@root/questions/actions";
import { updateRootContentId } from "@root/quizes/actions";
import { useUiTools } from "@root/uiTools/store";
interface Props {
switchState: string;
@ -40,7 +43,8 @@ export default function ButtonsOptions({
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isWrappMiniButtonSetting = useMediaQuery(theme.breakpoints.down(920));
const quiz = useCurrentQuiz();
const { openBranchingPanel, questions } = useQuestionsStore.getState();
const { questions } = useQuestionsStore.getState();
const { openBranchingPanel } = useUiTools();
const openedModal = () => {
updateOpenedModalSettingsId(question.id);
@ -64,15 +68,15 @@ export default function ButtonsOptions({
title: "Настройки",
value: "setting",
},
{
icon: (
<Clue
color={switchState === "help" ? "#ffffff" : theme.palette.grey3.main}
/>
),
title: "Подсказка",
value: "help",
},
// {
// icon: (
// <Clue
// color={switchState === "help" ? "#ffffff" : theme.palette.grey3.main}
// />
// ),
// title: "Подсказка",
// value: "help",
// },
{
icon: (
<Branching

@ -23,7 +23,7 @@ import ImgIcon from "../../assets/icons/questionsPage/imgIcon";
import SettingIcon from "../../assets/icons/questionsPage/settingIcon";
import { QuizQuestionVariant } from "@model/questionTypes/variant";
import { updateOpenedModalSettingsId } from "@root/questions/actions";
import { updateOpenBranchingPanel, updateDesireToOpenABranchingModal } from "@root/questions/actions";
import { updateOpenBranchingPanel, updateDesireToOpenABranchingModal } from "@root/uiTools/actions";
import { useQuestionsStore } from "@root/questions/store";
import { enqueueSnackbar } from "notistack";
import { useCurrentQuiz } from "@root/quizes/hooks";

@ -29,7 +29,7 @@ export default function SettingsData({ question }: SettingsDataProps) {
flexDirection: isWrappColumn ? "column" : null,
}}
>
<Box
{/* <Box
sx={{
pt: "20px",
pb: isMobile ? "25px" : "20px",
@ -67,7 +67,7 @@ export default function SettingsData({ question }: SettingsDataProps) {
});
}}
/>
</Box>
</Box> */}
<Box
sx={{
pt: isMobile ? "0px" : "20px",
@ -86,10 +86,10 @@ export default function SettingsData({ question }: SettingsDataProps) {
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Необязательный вопрос"}
checked={!question.required}
checked={!question.content.required}
handleChange={({ target }) => {
updateQuestion(question.id, question => {
question.required = !target.checked;
updateQuestion<QuizQuestionDate>(question.id, question => {
question.content.required = !target.checked;
});
}}
/>
@ -109,7 +109,7 @@ export default function SettingsData({ question }: SettingsDataProps) {
label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck}
handleChange={({ target }) => {
updateQuestion(question.id, question => {
updateQuestion<QuizQuestionDate>(question.id, question => {
question.content.innerNameCheck = target.checked;
question.content.innerName = target.checked ? question.content.innerName : "";
});

@ -3,8 +3,10 @@ import { Box, ListItem, Typography, useTheme } from "@mui/material";
import { memo, useEffect } from "react";
import { Draggable } from "react-beautiful-dnd";
import QuestionsPageCard from "./QuestionPageCard";
import { cancelQuestionDeletion, updateEditSomeQuestion } from "@root/questions/actions";
import { cancelQuestionDeletion } from "@root/questions/actions";
import { updateEditSomeQuestion } from "@root/uiTools/actions";
import { useQuestionsStore } from "@root/questions/store";
import { useUiTools } from "@root/uiTools/store";
type Props = {
@ -15,7 +17,7 @@ type Props = {
function DraggableListItem({ question, isDragging, index }: Props) {
const theme = useTheme();
const { editSomeQuestion } = useQuestionsStore();
const { editSomeQuestion } = useUiTools();
useEffect(() => {
if (editSomeQuestion !== null) {

@ -128,10 +128,10 @@ export default function SettingDropDown({ question }: SettingDropDownProps) {
</Typography>
<CustomCheckbox
label={"Необязательный вопрос"}
checked={!question.required}
checked={!question.content.required}
handleChange={(e) => {
updateQuestion(question.id, question => {
question.required = !e.target.checked;
updateQuestion<QuizQuestionSelect>(question.id, question => {
question.content.required = !e.target.checked;
});
}}
/>
@ -141,7 +141,7 @@ export default function SettingDropDown({ question }: SettingDropDownProps) {
label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck}
handleChange={({ target }) => {
updateQuestion(question.id, question => {
updateQuestion<QuizQuestionSelect>(question.id, question => {
question.content.innerNameCheck = target.checked;
question.content.innerName = target.checked ? question.content.innerName : "";
});

@ -30,7 +30,7 @@ export default function SettingEmoji({ question }: SettingEmojiProps) {
flexDirection: isWrappColumn ? "column" : "none",
}}
>
<Box
{/* <Box
sx={{
pt: isMobile ? "25px" : "20px",
pb: isMobile ? "25px" : "20px",
@ -66,12 +66,12 @@ export default function SettingEmoji({ question }: SettingEmojiProps) {
question.content.own = target.checked;
})}
/>
</Box>
</Box> */}
<Box
sx={{
pt: isMobile ? "0px" : "20px",
pb: "20px",
pl: isTablet ? "20px" : "",
pl: "20px",
pr: isFigmaTablte ? "30px" : "20px",
display: "flex",
flexDirection: "column",
@ -85,11 +85,11 @@ export default function SettingEmoji({ question }: SettingEmojiProps) {
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Необязательный вопрос"}
checked={!question.required}
handleChange={(e) => updateQuestion(question.id, question => {
checked={!question.content.required}
handleChange={({ target }) => updateQuestion<QuizQuestionEmoji>(question.id, question => {
if (question.type !== "emoji") return;
question.content.required = !e.target.checked;
question.content.required = !target.checked;
})}
/>
<Box
@ -107,7 +107,7 @@ export default function SettingEmoji({ question }: SettingEmojiProps) {
}}
label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck}
handleChange={({ target }) => updateQuestion(question.id, question => {
handleChange={({ target }) => updateQuestion<QuizQuestionEmoji>(question.id, question => {
question.content.innerNameCheck = target.checked;
question.content.innerName = target.checked ? question.content.innerName : "";
})}

@ -1,13 +1,11 @@
import { Box } from "@mui/material";
import { reorderQuestions } from "@root/questions/actions";
import { useQuestions } from "@root/questions/hooks";
import type { DropResult } from "react-beautiful-dnd";
import { DragDropContext, Droppable } from "react-beautiful-dnd";
import FormDraggableListItem from "./FormDraggableListItem";
export const FormDraggableList = () => {
const { questions } = useQuestions();
const onDragEnd = ({ destination, source }: DropResult) => {
if (destination) reorderQuestions(source.index, destination.index);

@ -111,11 +111,11 @@ export default function SettingOptionsAndPict({ question }: SettingOptionsAndPic
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Необязательный вопрос"}
checked={question.content.required}
handleChange={({ target }) => updateQuestion(question.id, question => {
checked={!question.content.required}
handleChange={({ target }) => updateQuestion<QuizQuestionVarImg>(question.id, question => {
if (question.type !== "varimg") return;
question.content.required = target.checked;
question.content.required = !target.checked;
})}
/>
<Box sx={{ display: "flex", alignItems: "center" }}>
@ -126,7 +126,7 @@ export default function SettingOptionsAndPict({ question }: SettingOptionsAndPic
}}
label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck}
handleChange={({ target }) => updateQuestion(question.id, question => {
handleChange={({ target }) => updateQuestion<QuizQuestionVarImg>(question.id, question => {
question.content.innerNameCheck = target.checked;
question.content.innerName = "";
})}

@ -64,7 +64,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
marginRight: isFigmaTablte ? (isMobile ? "0" : "0px") : "30px",
}}
>
<Box
{/* <Box
sx={{
pt: isMobile ? "25px" : "20px",
pb: isMobile ? "25px" : "20px",
@ -148,7 +148,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
})
}
/>
</Box>
</Box> */}
<Box
sx={{
pt: isMobile ? "25px" : "20px",
@ -161,7 +161,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
width: "100%",
}}
>
<Box
{/* <Box
sx={{
marginBottom: "5px",
opacity: question.content.xy !== "1:1" ? 1 : 0,
@ -202,7 +202,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
isActive={question.content.format === "masonry"}
Icon={FormatIcon1}
/>
</Box>
</Box> */}
<Typography
sx={{ fontWeight: "500", fontSize: "18px", color: " #4D4D4D" }}
>
@ -211,11 +211,11 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
<CustomCheckbox
sx={{ alignItems: isMobile ? "flex-start" : "" }}
label={"Необязательный вопрос"}
checked={question.content.required}
handleChange={({ target }) => updateQuestion(question.id, question => {
checked={!question.content.required}
handleChange={({ target }) => updateQuestion<QuizQuestionImages>(question.id, question => {
if (question.type !== "images") return;
question.content.required = target.checked;
question.content.required = !target.checked;
})
}
/>

@ -54,7 +54,7 @@ export default function SettingTextField({
marginRight: isFigmaTablte ? "0px" : "32px",
}}
>
<Box
{/* <Box
sx={{
pt: isMobile ? "25px" : "20px",
pb: isMobile ? "25px" : "20px",
@ -126,7 +126,7 @@ export default function SettingTextField({
});
}}
/>
</Box>
</Box> */}
<Box
sx={{
pt: isMobile ? "0px" : "20px",
@ -148,7 +148,7 @@ export default function SettingTextField({
>
Настройки вопросов
</Typography>
<CustomCheckbox
{/* <CustomCheckbox
sx={{
display: isMobile ? "flex" : "block",
mr: isMobile ? "0px" : "16px",
@ -161,7 +161,7 @@ export default function SettingTextField({
question.content.autofill = target.checked;
});
}}
/>
/> */}
<CustomCheckbox
sx={{
display: isMobile ? "flex" : "block",
@ -169,10 +169,10 @@ export default function SettingTextField({
alignItems: isMobile ? "flex-end" : "center",
}}
label={"Необязательный вопрос"}
checked={!question.required}
checked={!question.content.required}
handleChange={(e) => {
updateQuestion(question.id, question => {
question.required = !e.target.checked;
updateQuestion<QuizQuestionText>(question.id, question => {
question.content.required = !e.target.checked;
});
}}
/>
@ -193,7 +193,7 @@ export default function SettingTextField({
label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck}
handleChange={({ target }) => {
updateQuestion(question.id, question => {
updateQuestion<QuizQuestionText>(question.id, question => {
question.content.innerNameCheck = target.checked;
question.content.innerName = target.checked
? question.content.innerName

@ -1,18 +1,23 @@
import {
Box,
} from "@mui/material";
Box, useMediaQuery, useTheme,
} from "@mui/material";
import { DraggableList } from "./DraggableList";
import { SwitchBranchingPanel } from "./SwitchBranchingPanel";
import { BranchingMap } from "./BranchingMap";
import {useQuestionsStore} from "@root/questions/store";
import { useUiTools } from "@root/uiTools/store";
export const QuestionSwitchWindowTool = () => {
const {openBranchingPanel, questions} = useQuestionsStore.getState()
const {questions} = useQuestionsStore.getState()
const {openBranchingPanel} = useUiTools()
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
console.log("questions ", questions)
console.log("rules ", questions.filter((q) => q.type !== null).map((q) => ({id: q.content.id, rule: q.content.rule})))
return (
<Box sx={{ display: "flex", gap: "20px", flexWrap: "wrap" }}>
<Box sx={{ display: "flex", gap: "20px", flexWrap: "wrap", marginBottom: isMobile ? "20px" : undefined }}>
<Box sx={{ flexBasis: "796px" }}>
{openBranchingPanel? <BranchingMap /> : <DraggableList />}
</Box>

@ -17,12 +17,13 @@ import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
import BranchingQuestions from "./BranchingModal/BranchingQuestionsModal"
import { QuestionSwitchWindowTool } from "./QuestionSwitchWindowTool";
import { useQuestionsStore } from "@root/questions/store";
import { updateOpenBranchingPanel, updateEditSomeQuestion } from "@root/questions/actions";
import { updateOpenBranchingPanel, updateEditSomeQuestion } from "@root/uiTools/actions";
import { useUiTools } from "@root/uiTools/store";
export default function QuestionsPage() {
const theme = useTheme();
const { openedModalSettingsId, openBranchingPanel } = useQuestionsStore();
const { openedModalSettingsId, openBranchingPanel } = useUiTools();
const isMobile = false//useMediaQuery(theme.breakpoints.down(660));
const quiz = useCurrentQuiz();
useLayoutEffect(() => {

@ -148,12 +148,12 @@ export default function SettingSlider({ question }: SettingSliderProps) {
<CustomCheckbox
sx={{ display: "block", mr: isMobile ? "0px" : "16px" }}
label={"Необязательный вопрос"}
checked={!question.required}
checked={!question.content.required}
handleChange={(e) => {
updateQuestion(question.id, question => {
updateQuestion<QuizQuestionRating>(question.id, question => {
if (question.type !== "rating") return;
question.required = !e.target.checked;
question.content.required = !e.target.checked;
});
}}
/>

@ -78,12 +78,12 @@ export default function SettingSlider({ question }: SettingSliderProps) {
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px", alignItems: isMobile ? "flex-end" : "center" }}
label={"Необязательный вопрос"}
checked={!question.required}
checked={!question.content.required}
handleChange={(e) => {
updateQuestion(question.id, question => {
updateQuestion<QuizQuestionNumber>(question.id, question => {
if (question.type !== "number") return;
question.required = !e.target.checked;
question.content.required = !e.target.checked;
});
}}
/>

@ -2,11 +2,11 @@ import { useParams } from "react-router-dom";
import { Box, Button, IconButton, Typography } from "@mui/material";
import { ReactComponent as CheckedIcon } from "@icons/checked.svg";
import { useQuestionsStore } from "@root/questions/store";
import { updateDragQuestionContentId } from "@root/questions/actions";
import { useEffect } from "react";
import { AnyTypedQuizQuestion, UntypedQuizQuestion } from "@model/questionTypes/shared";
import { Pencil } from "../../startPage/Sidebar/icons/Pencil";
import {updateOpenBranchingPanel, updateEditSomeQuestion} from "@root/questions/actions"
import { updateOpenBranchingPanel, updateEditSomeQuestion, updateDragQuestionContentId } from "@root/uiTools/actions"
import { useUiTools } from "@root/uiTools/store";
const getItemStyle = (isDragging: any, draggableStyle: any) => ({
@ -24,7 +24,7 @@ const getItemStyle = (isDragging: any, draggableStyle: any) => ({
type AnyQuestion = UntypedQuizQuestion | AnyTypedQuizQuestion
export const QuestionsList = () => {
const { desireToOpenABranchingModal } = useQuestionsStore()
const { desireToOpenABranchingModal } = useUiTools()
const trashQuestions = useQuestionsStore().questions
const questions = trashQuestions.filter((question) => question.type !== "result")

@ -1,17 +1,23 @@
import {Box, Typography, Switch, useTheme, Button, useMediaQuery} from "@mui/material";
import { QuestionsList } from "./QuestionsList";
import { updateOpenBranchingPanel } from "@root/questions/actions";
import { updateOpenBranchingPanel } from "@root/uiTools/actions";
import {useQuestionsStore} from "@root/questions/store";
import {useRef} from "react";
import { useUiTools } from "@root/uiTools/store";
export const SwitchBranchingPanel = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(660));
const {openBranchingPanel} = useQuestionsStore.getState()
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const {openBranchingPanel} = useUiTools()
const ref = useRef()
return (
return ( !isTablet || openBranchingPanel ?
<Box sx={{ userSelect: "none", maxWidth: "350px", width: "100%" }}>
<Box
sx={{
@ -25,10 +31,10 @@ export const SwitchBranchingPanel = () => {
}}
>
<Switch
value={openBranchingPanel}
onChange={(_, value) => {
updateOpenBranchingPanel(value)
}}
checked={openBranchingPanel}
onChange={
(e) => updateOpenBranchingPanel(e.target.checked)
}
sx={{
width: 50,
height: 30,
@ -85,5 +91,7 @@ export const SwitchBranchingPanel = () => {
</Box>
{ openBranchingPanel && <QuestionsList /> }
</Box>
:
<></>
);
};

@ -40,7 +40,7 @@ export default function SettingsUpload({ question }: SettingsUploadProps) {
}}
>
<Typography>Настройки вопроса</Typography>
<CustomCheckbox
{/* <CustomCheckbox
sx={{
display: isMobile ? "flex" : "block",
mr: isMobile ? "0px" : "16px",
@ -52,17 +52,17 @@ export default function SettingsUpload({ question }: SettingsUploadProps) {
question.content.autofill = target.checked;
});
}}
/>
/> */}
<CustomCheckbox
sx={{
display: isMobile ? "flex" : "block",
mr: isMobile ? "0px" : "16px",
}}
label={"Необязательный вопрос"}
checked={!question.required}
checked={!question.content.required}
handleChange={(e) => {
updateQuestion(question.id, question => {
question.required = !e.target.checked;
updateQuestion<QuizQuestionFile>(question.id, question => {
question.content.required = !e.target.checked;
});
}}
/>
@ -82,7 +82,7 @@ export default function SettingsUpload({ question }: SettingsUploadProps) {
label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck}
handleChange={({ target }) => {
updateQuestion(question.id, question => {
updateQuestion<QuizQuestionFile>(question.id, question => {
question.content.innerNameCheck = target.checked;
question.content.innerName = target.checked ? question.content.innerName : "";
});

@ -1,9 +1,9 @@
import {
Box,
Tooltip,
Typography,
useMediaQuery,
useTheme,
Box,
Tooltip,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { setQuestionInnerName, updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
@ -12,165 +12,166 @@ import { useDebouncedCallback } from "use-debounce";
import InfoIcon from "../../../assets/icons/InfoIcon";
import type { QuizQuestionVariant } from "../../../model/questionTypes/variant";
interface Props {
question: QuizQuestionVariant;
question: QuizQuestionVariant;
}
export default function ResponseSettings({ question }: Props) {
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(900));
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(900));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const updateQuestionInnerName = useDebouncedCallback((value) => {
setQuestionInnerName(question.id, value);
}, 200);
const updateQuestionInnerName = useDebouncedCallback((value) => {
setQuestionInnerName(question.id, value);
}, 200);
return (
<Box
sx={{
display: "flex",
justifyContent: "space-between",
flexDirection: isTablet ? "column" : "none",
marginRight: isFigmaTablte ? (isMobile ? "0" : "0px") : "30px",
}}
return (
<Box
sx={{
display: "flex",
justifyContent: "space-between",
flexDirection: isTablet ? "column" : "none",
marginRight: isFigmaTablte ? (isMobile ? "0" : "0px") : "30px",
}}
>
<Box
sx={{
pt: isMobile ? "25px" : "20px",
pb: isMobile ? "25px" : "20px",
pl: "20px",
display: "flex",
flexDirection: "column",
gap: "14px",
width: "100%",
}}
>
<Typography
sx={{
height: isMobile ? "18px" : "auto",
fontWeight: "500",
fontSize: "18px",
color: " #4D4D4D",
}}
>
<Box
sx={{
pt: isMobile ? "25px" : "20px",
pb: isMobile ? "25px" : "20px",
pl: "20px",
display: "flex",
flexDirection: "column",
gap: "14px",
width: "100%",
}}
Настройки ответов
</Typography>
{/* <CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Длинный текстовый ответ"}
checked={question.content.largeCheck}
handleChange={({ target }) => {
updateQuestion(question.id, (question) => {
if (!("largeCheck" in question.content)) return;
question.content.largeCheck = target.checked;
});
}}
/> */}
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Можно несколько"}
checked={question.content.multi}
dataCy="multiple-answers-checkbox"
handleChange={({ target }) => {
updateQuestion(question.id, (question) => {
if (!("multi" in question.content)) return;
question.content.multi = target.checked;
});
}}
/>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={'Вариант "свой ответ"'}
checked={question.content.own}
handleChange={({ target }) => {
updateQuestion(question.id, (question) => {
if (!("own" in question.content)) return;
question.content.own = target.checked;
});
}}
/>
</Box>
<Box
sx={{
boxSizing: "border-box",
pt: isMobile ? "0px" : "20px",
pb: "20px",
pl: isFigmaTablte ? (isTablet ? "20px" : "34px") : "28px",
pr: isFigmaTablte ? "19px" : "20px",
display: "flex",
flexDirection: "column",
gap: "14px",
width: "100%",
}}
>
<Typography
sx={{
height: isMobile ? "18px" : "auto",
fontWeight: "500",
fontSize: "18px",
color: " #4D4D4D",
}}
>
Настройки вопросов
</Typography>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Необязательный вопрос"}
checked={!question.content.required}
handleChange={({ target }) => {
updateQuestion<QuizQuestionVariant>(question.id, (question) => {
question.content.required = !target.checked;
});
}}
/>
<Box
sx={{
width: isMobile ? "90%" : "auto",
display: "flex",
alignItems: "center",
}}
>
<CustomCheckbox
sx={{
mr: isMobile ? "0px" : "9px",
height: isMobile ? "42px" : "26px",
alignItems: "start",
}}
label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck}
handleChange={({ target }) => {
updateQuestion<QuizQuestionVariant>(question.id, (question) => {
question.content.innerNameCheck = target.checked;
question.content.innerName = target.checked
? question.content.innerName
: "";
});
}}
/>
{isMobile && (
<Tooltip
title="Будет отображаться как заголовок вопроса в приходящих заявках."
placement="top"
>
<Typography
sx={{
height: isMobile ? "18px" : "auto",
fontWeight: "500",
fontSize: "18px",
color: " #4D4D4D",
}}
>
Настройки ответов
</Typography>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Длинный текстовый ответ"}
checked={question.content.largeCheck}
handleChange={({ target }) => {
updateQuestion(question.id, question => {
if (!("largeCheck" in question.content)) return;
question.content.largeCheck = target.checked;
});
}}
/>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Можно несколько"}
checked={question.content.multi}
dataCy="multiple-answers-checkbox"
handleChange={({ target }) => {
updateQuestion(question.id, question => {
if (!("multi" in question.content)) return;
question.content.multi = target.checked;
});
}}
/>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={'Вариант "свой ответ"'}
checked={question.content.own}
handleChange={({ target }) => {
updateQuestion(question.id, question => {
if (!("own" in question.content)) return;
question.content.own = target.checked;
});
}}
/>
</Box>
<Box
sx={{
boxSizing: "border-box",
pt: isMobile ? "0px" : "20px",
pb: "20px",
pl: isFigmaTablte ? (isTablet ? "20px" : "34px") : "28px",
pr: isFigmaTablte ? "19px" : "20px",
display: "flex",
flexDirection: "column",
gap: "14px",
width: "100%",
}}
>
<Typography
sx={{
height: isMobile ? "18px" : "auto",
fontWeight: "500",
fontSize: "18px",
color: " #4D4D4D",
}}
>
Настройки вопросов
</Typography>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Необязательный вопрос"}
checked={!question.required}
handleChange={({ target }) => {
updateQuestion(question.id, question => {
question.required = !target.checked;
});
}}
/>
<Box
sx={{
width: isMobile ? "90%" : "auto",
display: "flex",
alignItems: "center",
}}
>
<CustomCheckbox
sx={{
mr: isMobile ? "0px" : "9px",
height: isMobile ? "42px" : "26px",
alignItems: "start",
}}
label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck}
handleChange={({ target }) => {
updateQuestion(question.id, question => {
question.content.innerNameCheck = target.checked;
question.content.innerName = target.checked ? question.content.innerName : "";
});
}}
/>
{isMobile && (
<Tooltip
title="Будет отображаться как заголовок вопроса в приходящих заявках."
placement="top"
>
<Box>
<InfoIcon />
</Box>
</Tooltip>
)}
</Box>
{question.content.innerNameCheck && (
<CustomTextField
sx={{ mr: isMobile ? "0px" : "16px" }}
placeholder={"Развёрнутое описание вопроса"}
text={question.content.innerName}
onChange={({ target }) => updateQuestionInnerName(target.value)}
/>
)}
</Box>
<Box>
<InfoIcon />
</Box>
</Tooltip>
)}
</Box>
);
{question.content.innerNameCheck && (
<CustomTextField
sx={{ mr: isMobile ? "0px" : "16px" }}
placeholder={"Развёрнутое описание вопроса"}
text={question.content.innerName}
onChange={({ target }) => updateQuestionInnerName(target.value)}
/>
)}
</Box>
</Box>
);
}

@ -16,17 +16,14 @@ export const FirstEntry = () => {
const create = () => {
if (quiz?.config.haveRoot) {
console.log("createFrontResult")
questions
.filter((question:AnyTypedQuizQuestion) => {
console.log(question)
return question.type !== null && question.content.rule.parentId.length !== 0 && question.content.rule.children.length === 0
})
.forEach(question => {
createFrontResult(quiz.id, question.content.id)
})
} else {
console.log("createFrontResult")
createFrontResult(quiz.id, "line")
}
}

@ -21,7 +21,6 @@ export const ResultSettings = () => {
const { questions } = useQuestionsStore()
const quiz = useCurrentQuiz()
const results = useQuestionsStore().questions.filter((q): q is QuizQuestionResult => q.type === "result")
console.log("опросник ", quiz)
const [quizExpand, setQuizExpand] = useState(true)
const [resultContract, setResultContract] = useState(true)
const isReadyToLeaveRef = useRef(true);

@ -3,17 +3,14 @@ import { Box, Button, useTheme } from "@mui/material";
import { useQuizViewStore } from "@root/quizView";
import type {
AnyTypedQuizQuestion,
QuizQuestionBase,
} from "../../model/questionTypes/shared";
import type { AnyTypedQuizQuestion, QuizQuestionBase } from "../../model/questionTypes/shared";
import { getQuestionByContentId } from "@root/questions/actions";
import { enqueueSnackbar } from "notistack";
type FooterProps = {
questions: AnyTypedQuizQuestion[];
setCurrentQuestion: (step: AnyTypedQuizQuestion) => void;
question: QuizQuestionBase;
question: AnyTypedQuizQuestion;
};
export const Footer = ({
@ -21,9 +18,6 @@ export const Footer = ({
questions,
question,
}: FooterProps) => {
const [disabledQuestionsId, setDisabledQuestionsId] = useState<Set<string>>(
new Set()
);
const [disablePreviousButton, setDisablePreviousButton] =
useState<boolean>(false);
const [disableNextButton, setDisableNextButton] = useState<boolean>(false);
@ -58,11 +52,17 @@ export const Footer = ({
({ questionId }) => questionId === question.content.id
);
if (question.required && answer?.changed) {
if ("required" in question.content && question.content.required && answer) {
setDisableNextButton(false);
return;
}
if (question.required && !answer?.changed) {
if (
"required" in question.content &&
question.content.required &&
!answer
) {
setDisableNextButton(true);
return;
@ -103,7 +103,7 @@ export const Footer = ({
let readyBeNextQuestion = "";
question.content.rule.main.forEach(({ next, rules }) => {
(question as QuizQuestionBase).content.rule.main.forEach(({ next, rules }) => {
let longerArray = Math.max(
rules[0].answers.length,
[answer?.answer].length

@ -1,162 +1,298 @@
import { useParams } from "react-router-dom";
import {
Box,
Button,
ButtonBase,
Link,
Paper,
Typography,
useTheme,
useMediaQuery,
useTheme,
} from "@mui/material";
import useSWR from "swr";
import { isAxiosError } from "axios";
import { enqueueSnackbar } from "notistack";
import { devlog } from "@frontend/kitui";
import { useCurrentQuiz } from "@root/quizes/hooks";
import YoutubeEmbedIframe from "../../ui_kit/StartPagePreview/YoutubeEmbedIframe";
import { QuizStartpageAlignType, QuizStartpageType } from "@model/quizSettings";
import { notReachable } from "../../utils/notReachable";
import { useUADevice } from "../../utils/hooks/useUADevice";
import { quizApi } from "@api/quiz";
interface Props {
setVisualStartPage: (a:boolean) => void
}
import { useQuizStore } from "@root/quizes/store";
import { useQuestions } from "@root/questions/hooks";
import { setQuizes } from "@root/quizes/actions";
type StartPageViewPublicationProps = {
setVisualStartPage: (bool: boolean) => void;
showNextButton:boolean
};
export const StartPageViewPublication = ({
setVisualStartPage,
showNextButton
}: StartPageViewPublicationProps) => {
const quizId = Number(useParams().quizId);
const { quizes } = useQuizStore();
const { questions } = useQuestions();
export const StartPageViewPublication = ({setVisualStartPage}:Props) => {
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(630));
const quiz = quizes.find(({ backendId }) => quizId === backendId);
const isMediaFileExist =
quiz?.config.startpage.background.desktop ||
quiz?.config.startpage.background.video;
const quiz = useCurrentQuiz();
const { isMobileDevice } = useUADevice();
useSWR("quizes", () => quizApi.getList(), {
onSuccess: setQuizes,
onError: (error: unknown) => {
const message = isAxiosError<string>(error)
? error.response?.data ?? ""
: "";
if (!quiz) return null;
devlog("Error getting quiz list", error);
enqueueSnackbar(`Не удалось получить квизы. ${message}`);
},
});
const handleCopyNumber = () => {
navigator.clipboard.writeText(quiz.config.info.phonenumber);
};
const background = quiz.config.startpage.background.type === "image"
? quiz.config.startpage.background.desktop
? (
<img
src={quiz.config.startpage.background.desktop}
alt=""
style={{
width: "100%",
height: "100%",
objectFit: "cover",
overflow: "hidden"
}}
/>
)
: null
: quiz.config.startpage.background.type === "video"
? quiz.config.startpage.background.video
? (
<YoutubeEmbedIframe videoUrl={quiz.config.startpage.background.video}
containerSX={{
width: quiz.config.startpageType === "centered" ? "550px" : quiz.config.startpageType === "expanded" ? "100vw" : "100%",
height: quiz.config.startpageType === "centered" ? "275px" : quiz.config.startpageType === "expanded" ? "100vh" : "100%",
borderRadius: quiz.config.startpageType === "centered" ? "10px" : "0",
overflow: "hidden",
"& iframe": {
width: "100%",
height: "100%",
transform: quiz.config.startpageType === "centered" ? "" : quiz.config.startpageType === "expanded" ? "scale(1.5)" : "scale(2.4)",
}
}}
/>
)
: null
: null;
return (
<Box
<Paper className="quiz-preview-draghandle"
sx={{
height: "100vh",
display: "flex",
flexDirection:
quiz?.config.startpage.position === "left" ? "row" : "row-reverse",
flexGrow: 1,
"&::-webkit-scrollbar": { width: 0 },
}}
>
<Box
sx={{
width: isMediaFileExist && !isTablet ? "40%" : "100%",
padding: "16px",
display: "flex",
flexDirection: "column",
alignItems: isMediaFileExist && !isTablet ? "flex-start" : "center",
}}
>
<Box
sx={{
background: quiz.config.startpageType === "expanded" ?
quiz.config.startpage.position === "left" ? "linear-gradient(90deg,#272626,transparent)" :
quiz.config.startpage.position === "center" ? "linear-gradient(180deg,transparent,#272626)" :
"linear-gradient(270deg,#272626,transparent)"
: "",
color: quiz.config.startpageType === "expanded" ? "white" : "black"
}}>
<QuizPreviewLayoutByType
quizHeaderBlock={<Box
p={quiz.config.startpageType === "standard" ? "" : "16px"}
>
<Box sx={{
display: "flex",
alignItems: "center",
gap: "20px",
}}
>
{quiz?.config.startpage.background.mobile && (
<img
src={quiz.config.startpage.background.mobile}
style={{
height: "50px",
maxWidth: "100px",
objectFit: "cover",
}}
alt=""
/>
)}
<Typography sx={{ fontSize: "18px" }}>
{quiz?.config.info.orgname}
</Typography>
</Box>
<Box
sx={{
flexGrow: 1,
display: "flex",
gap: "10px",
flexDirection: "column",
justifyContent: "center",
}}
>
<Typography sx={{ fontWeight: "bold", fontSize: "20px" }}>
{quiz?.name}
</Typography>
<Typography sx={{ fontSize: "16px" }}>
{quiz?.config.startpage.description}
</Typography>
<Box>
<Button
variant="contained"
sx={{ fontSize: "16px", padding: "10px 15px" }}
// disabled={!questions.length}
onClick={() => setVisualStartPage(false)}
>
{quiz?.config.startpage.button
? quiz?.config.startpage.button
: "Пройти тест"}
</Button>
</Box>
</Box>
<Box>
<Typography
sx={{ fontSize: "16px", color: theme.palette.brightPurple.main }}
>
{quiz?.config.info.phonenumber}
</Typography>
<Typography sx={{ fontSize: "12px" }}>
{quiz?.config.info.law}
</Typography>
</Box>
</Box>
{!isTablet && isMediaFileExist && (
<Box sx={{ width: "60%" }}>
{quiz?.config.startpage.background.mobile && (
<img
src={quiz.config.startpage.background.mobile}
alt=""
style={{
display: "block",
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
)}
{quiz.config.startpage.background.type === "video" &&
quiz.config.startpage.background.video && (
<video
src={quiz.config.startpage.background.video}
controls
mb: "7px"
}}>
{quiz.config.startpage.logo && (
<img
src={quiz.config.startpage.logo}
style={{
width: "100%",
height: "100%",
height: "37px",
maxWidth: "43px",
objectFit: "cover",
}}
alt=""
/>
)}
</Box>
)}
</Box>
<Typography sx={{ fontSize: "14px" }}>
{quiz.config.info.orgname}
</Typography>
</Box>
<Link mb="16px" href={quiz.config.info.site}>
<Typography sx={{ fontSize: "16px", color: theme.palette.brightPurple.main }}>
{quiz.config.info.site}
</Typography>
</Link>
</Box>}
quizMainBlock={<>
<Box sx={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: quiz.config.startpageType === "centered" ? "center" :
quiz.config.startpageType === "expanded"
? quiz.config.startpage.position === "center" ?
"center"
: "start": "start",
mt: "28px",
width: "100%"
}}>
<Typography sx={{
fontWeight: "bold",
fontSize: "26px",
fontStyle: "normal",
fontStretch: "normal",
lineHeight: "1.2",
}}>{quiz.name}</Typography>
<Typography sx={{
fontSize: "16px",
m: "16px 0"
}}>
{quiz.config.startpage.description}
</Typography>
<Box width={ quiz.config.startpageType === "standard" ? "100%" : "auto"}>
<Button
variant="contained"
sx={{
fontSize: "16px",
padding: "10px 15px",
width: quiz.config.startpageType === "standard" ? "100%" : "auto"
}}
onClick={() => setVisualStartPage(false)}
>
{quiz.config.startpage.button.trim() ? quiz.config.startpage.button : "Пройти тест"}
</Button>
</Box>
</Box>
<Box
sx={{
mt: "46px"
}}
>
{quiz.config.info.clickable ? (
isMobileDevice ? (
<Link href={`tel:${quiz.config.info.phonenumber}`}>
<Typography sx={{ fontSize: "16px", color: theme.palette.brightPurple.main }}>
{quiz.config.info.phonenumber}
</Typography>
</Link>
) : (
<ButtonBase onClick={handleCopyNumber}>
<Typography sx={{ fontSize: "16px", color: theme.palette.brightPurple.main }}>
{quiz.config.info.phonenumber}
</Typography>
</ButtonBase>
)
) : (
<Typography sx={{ fontSize: "16px", color: theme.palette.brightPurple.main }}>
{quiz.config.info.phonenumber}
</Typography>
)}
<Typography sx={{ fontSize: "12px", textAlign: "end" }}>
{quiz.config.info.law}
</Typography>
</Box>
</>}
backgroundBlock={background}
startpageType={quiz.config.startpageType}
alignType={quiz.config.startpage.position}
/>
</Paper>
);
}
function QuizPreviewLayoutByType({ quizHeaderBlock, quizMainBlock, backgroundBlock, startpageType, alignType }: {
quizHeaderBlock: JSX.Element;
quizMainBlock: JSX.Element;
backgroundBlock: JSX.Element | null;
startpageType: QuizStartpageType;
alignType: QuizStartpageAlignType;
}) {
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(630));
switch (startpageType) {
case null:
case "standard": {
return (
<Box sx={{
display: "flex",
flexDirection: alignType === "left" ? "row" : "row-reverse",
flexGrow: 1,
height: "100vh",
"&::-webkit-scrollbar": { width: 0 },
}}>
<Box sx={{
width: !isTablet ? "40%" : "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: !isTablet ? "flex-start" : "center",
p: "25px"
}}>
{quizHeaderBlock}
{quizMainBlock}
</Box>
<Box sx={{
width: "60%",
overflow: "hidden"
}}>
{backgroundBlock}
</Box>
</Box>
);
}
case "expanded": {
return (
<Box sx={{
position: "relative",
display: "flex",
justifyContent: startpageAlignTypeToJustifyContent[alignType],
flexGrow: 1,
height: "100%",
"&::-webkit-scrollbar": { width: 0 },
}}>
<Box sx={{
width: "40%",
position: "relative",
padding: "16px",
zIndex: 2,
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: alignType === "center" ? "center" : "start",
}}>
{quizHeaderBlock}
{quizMainBlock}
</Box>
<Box sx={{
position: "absolute",
left: 0,
top: 0,
height: "100%",
width: "100%",
zIndex: 1,
overflow: "hidden"
}}>
{backgroundBlock}
</Box>
</Box>
);
}
case "centered": {
return (
<Box sx={{
padding: "16px",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
height: "100%",
"&::-webkit-scrollbar": { width: 0 },
overflow: "hidden"
}}>
{quizHeaderBlock}
{backgroundBlock &&
<Box>
{backgroundBlock}
</Box>
}
{quizMainBlock}
</Box>
);
}
default: notReachable(startpageType);
}
}
const startpageAlignTypeToJustifyContent: Record<QuizStartpageAlignType, "start" | "center" | "end"> = {
left: "start",
center: "center",
right: "end",
};

@ -5,15 +5,40 @@ import { StartPageViewPublication } from "./StartPageViewPublication";
import { Question } from "./Question";
import { useQuestions } from "@root/questions/hooks";
import { useCurrentQuiz } from "@root/quizes/hooks";
import useSWR from "swr";
import { quizApi } from "@api/quiz";
import { setQuizes } from "@root/quizes/actions";
import { isAxiosError } from "axios";
import { devlog } from "@frontend/kitui";
import { useQuizStore } from "@root/quizes/store";
import type { AnyTypedQuizQuestion } from "../../model/questionTypes/shared";
import { enqueueSnackbar } from "notistack";
import { useQuestionsStore } from "@root/questions/store";
import { setQuestions } from "@root/questions/actions";
import { questionApi } from "@api/question";
export const ViewPage = () => {
const quiz = useCurrentQuiz();
const { questions } = useQuestions();
const [visualStartPage, setVisualStartPage] = useState<boolean>(
!quiz?.config.noStartPage
);
const { editQuizId } = useQuizStore();
const { questions } = useQuestionsStore();
useEffect(() => {
const getData = async () => {
const quizes = await quizApi.getList()
setQuizes(quizes)
const questions = await questionApi.getList({ quiz_id: editQuizId })
setQuestions(questions)
}
getData()
}, [])
useEffect(() => {
setVisualStartPage(quiz?.config.noStartPage)
}, [questions])
const [visualStartPage, setVisualStartPage] = useState<boolean>();
useEffect(() => {
const link = document.querySelector('link[rel="icon"]');
@ -27,13 +52,12 @@ export const ViewPage = () => {
questions.filter(({ type }) => type) as AnyTypedQuizQuestion[]
).sort((previousItem, item) => previousItem.page - item.page);
console.log("visualStartPage ", visualStartPage)
if (visualStartPage === undefined) return <></>
return (
<Box>
{visualStartPage ? (
<StartPageViewPublication
setVisualStartPage={setVisualStartPage}
showNextButton={!!filteredQuestions.length}
/>
{!visualStartPage ? (
<StartPageViewPublication setVisualStartPage={setVisualStartPage}/>
) : (
<Question questions={filteredQuestions} />
)}

@ -4,8 +4,6 @@ import { Box, Typography } from "@mui/material";
import { useQuizViewStore, updateAnswer } from "@root/quizView";
// import "react-datepicker/dist/react-datepicker.css";
import type { QuizQuestionDate } from "../../../model/questionTypes/date";
import CalendarIcon from "@icons/CalendarIcon";

@ -41,17 +41,77 @@ export const File = ({ currentQuestion }: FileProps) => {
maxWidth: answer?.split("|")[0] ? "640px" : "550px"
}}
>
<ButtonBase component="label" sx={{ justifyContent: "flex-start" }}>
<input
onChange={uploadFile}
hidden
accept={UPLOAD_FILE_TYPES_MAP[currentQuestion.content.type]}
multiple
type="file"
/>
<UploadBox icon={<UploadIcon />} text="5 MB максимум" />
</ButtonBase>
{answer && currentQuestion.content.type === "picture" && (
{answer?.split("|")[0] && (
<Box sx={{display: "flex", alignItems: "center", gap: "15px"}}>
<Typography>Вы загрузили:</Typography>
<Box sx={{padding: "5px 5px 5px 16px", backgroundColor: theme.palette.brightPurple.main,
borderRadius: "8px",
color: "#FFFFFF",
display: "flex",
alignItems: "center",
gap: "15px"
}}>
<Typography>
{answer?.split("|")[0]}
</Typography>
<IconButton
sx={{p: 0}}
onClick={() => {updateAnswer(currentQuestion.content.id, "");}}
>
<X/>
</IconButton>
</Box>
</Box>
)}
{!answer?.split("|")[0] && (
<ButtonBase component="label" sx={{ justifyContent: "flex-start" }}>
<input
onChange={uploadFile}
hidden
accept={UPLOAD_FILE_TYPES_MAP[currentQuestion.content.type]}
multiple
type="file"
/>
<Box
onDragOver={(event: DragEvent<HTMLDivElement>) => event.preventDefault()}
sx={{
width: "100%",
height: "120px",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "33px 44px 33px 55px",
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.grey2.main}`,
borderRadius: "8px",
}}
>
<UploadIcon />
<Box>
<Typography
sx={{
color: theme.palette.grey2.main,
fontWeight: 500
}}
>Добавить видео</Typography>
<Typography
sx={{
color: theme.palette.grey2.main,
fontSize: "16px",
lineHeight: "19px",
}}
>
Принимает .mp4 и .mov формат максимум 100мб
</Typography>
</Box>
</Box>
</ButtonBase>
)}
{answer && currentQuestion.content.type === "picture" && (
<img
src={answer.split("|")[1]}
alt=""
@ -73,11 +133,6 @@ export const File = ({ currentQuestion }: FileProps) => {
}}
/>
)}
{answer?.split("|")[0] && (
<Typography sx={{ marginTop: "15px" }}>
{answer?.split("|")[0]}
</Typography>
)}
</Box>
</Box>
);

@ -8,6 +8,7 @@ import { CustomSlider } from "@ui_kit/CustomSlider";
import { useQuizViewStore, updateAnswer } from "@root/quizView";
import type { QuizQuestionNumber } from "../../../model/questionTypes/number";
import {CustomSlider} from "@ui_kit/CustomSlider";
type NumberProps = {
currentQuestion: QuizQuestionNumber;
@ -47,7 +48,6 @@ export const Number = ({ currentQuestion }: NumberProps) => {
const max = window.Number(currentQuestion.content.range.split("—")[1]);
useEffect(() => {
console.log("ans", currentQuestion.content.start);
if (answer) {
setMinRange(answer.split("—")[0]);
setMaxRange(answer.split("—")[1]);
@ -57,8 +57,7 @@ export const Number = ({ currentQuestion }: NumberProps) => {
currentQuestion.content.id,
currentQuestion.content.chooseRange
? `${currentQuestion.content.start}${max}`
: String(currentQuestion.content.start),
false
: String(currentQuestion.content.start)
);
setMinRange(String(currentQuestion.content.start));

@ -7,6 +7,12 @@ import {
import { useQuizViewStore, updateAnswer } from "@root/quizView";
import TropfyIcon from "@icons/questionsPage/tropfyIcon";
import FlagIcon from "@icons/questionsPage/FlagIcon";
import HeartIcon from "@icons/questionsPage/heartIcon";
import LikeIcon from "@icons/questionsPage/likeIcon";
import LightbulbIcon from "@icons/questionsPage/lightbulbIcon";
import HashtagIcon from "@icons/questionsPage/hashtagIcon";
import StarIconMini from "@icons/questionsPage/StarIconMini";
import type { QuizQuestionRating } from "../../../model/questionTypes/rating";
@ -15,6 +21,37 @@ type RatingProps = {
currentQuestion: QuizQuestionRating;
};
const buttonRatingForm = [
{
name: "star",
icon: (color: string) => <StarIconMini width={50} color={color} />,
},
{
name: "trophie",
icon: (color: string) => <TropfyIcon color={color} />,
},
{
name: "flag",
icon: (color: string) => <FlagIcon color={color} />,
},
{
name: "heart",
icon: (color: string) => <HeartIcon color={color} />,
},
{
name: "like",
icon: (color: string) => <LikeIcon color={color} />,
},
{
name: "bubble",
icon: (color: string) => <LightbulbIcon color={color} />,
},
{
name: "hashtag",
icon: (color: string) => <HashtagIcon color={color} />,
},
];
export const Rating = ({ currentQuestion }: RatingProps) => {
const { answers } = useQuizViewStore();
const theme = useTheme();
@ -22,54 +59,44 @@ export const Rating = ({ currentQuestion }: RatingProps) => {
answers.find(
({ questionId }) => questionId === currentQuestion.content.id
) ?? {};
const form = buttonRatingForm.find(
({ name }) => name === currentQuestion.content.form
);
return (
<Box>
<Typography variant="h5">{currentQuestion.title}</Typography>
<Box
sx={{
display: "inline-block",
width: "100%",
display: "inline-flex",
alignItems: "center",
gap: "20px",
marginTop: "20px",
}}
>
<RatingComponent
value={Number(answer || 0)}
onChange={(_, value) =>
updateAnswer(currentQuestion.content.id, String(value))
}
sx={{ height: "50px", gap: "15px" }}
max={currentQuestion.content.steps}
icon={
<StarIconMini
color={theme.palette.brightPurple.main}
width={50}
sx={{ transform: "scale(1.4)" }}
/>
}
emptyIcon={
<StarIconMini
color={theme.palette.grey2.main}
width={50}
sx={{ transform: "scale(1.4)" }}
/>
}
/>
<Typography sx={{ color: theme.palette.grey2.main }}>
{currentQuestion.content.ratingNegativeDescription}
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
maxWidth: `${currentQuestion.content.steps * 50}px`,
color: theme.palette.grey2.main,
display: "inline-block",
width: "100%",
}}
>
<Typography>
{currentQuestion.content.ratingNegativeDescription}
</Typography>
<Typography>
{currentQuestion.content.ratingPositiveDescription}
</Typography>
<RatingComponent
value={Number(answer || 0)}
onChange={(_, value) =>
updateAnswer(currentQuestion.content.id, String(value))
}
sx={{ height: "50px", gap: "15px" }}
max={currentQuestion.content.steps}
icon={form?.icon(theme.palette.brightPurple.main)}
emptyIcon={form?.icon(theme.palette.grey2.main)}
/>
</Box>
<Typography sx={{ color: theme.palette.grey2.main }}>
{currentQuestion.content.ratingPositiveDescription}
</Typography>
</Box>
</Box>
);

@ -1,3 +1,4 @@
import { useEffect } from "react";
import {
Box,
Typography,
@ -6,31 +7,56 @@ import {
FormControlLabel,
Radio,
Checkbox,
TextField,
useTheme,
} from "@mui/material";
import { useQuizViewStore, updateAnswer, deleteAnswer } from "@root/quizView";
import {
useQuizViewStore,
updateAnswer,
deleteAnswer,
updateOwnVariant,
deleteOwnVariant,
} from "@root/quizView";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
import { CheckboxIcon } from "@icons/Checkbox";
import type { QuizQuestionVariant } from "../../../model/questionTypes/variant";
import type { QuestionVariant } from "../../../model/questionTypes/shared";
type VariantProps = {
stepNumber: number;
currentQuestion: QuizQuestionVariant;
};
type VariantItemProps = {
currentQuestion: QuizQuestionVariant;
variant: QuestionVariant;
answer: string | string[] | undefined;
index: number;
own?: boolean;
};
export const Variant = ({ currentQuestion }: VariantProps) => {
const { answers } = useQuizViewStore();
const theme = useTheme();
const { answers, ownVariants } = useQuizViewStore();
const { answer } =
answers.find(
({ questionId }) => questionId === currentQuestion.content.id
) ?? {};
const ownVariant = ownVariants.find(
(variant) => variant.contentId === currentQuestion.content.id
);
const Group = currentQuestion.content.multi ? FormGroup : RadioGroup;
useEffect(() => {
if (!ownVariant) {
updateOwnVariant(currentQuestion.content.id, "");
}
}, []);
return (
<Box>
<Typography variant="h5">{currentQuestion.title}</Typography>
@ -59,58 +85,23 @@ export const Variant = ({ currentQuestion }: VariantProps) => {
}}
>
{currentQuestion.content.variants.map((variant, index) => (
<FormControlLabel
<VariantItem
key={variant.id}
sx={{
margin: "0",
borderRadius: "12px",
padding: "15px",
border: `1px solid ${theme.palette.grey2.main}`,
display: "flex",
maxWidth: "685px",
justifyContent: "space-between",
width: "100%",
}}
value={index}
labelPlacement="start"
control={
currentQuestion.content.multi ? (
<Checkbox
checked={!!answer?.includes(variant.id)}
checkedIcon={<RadioCheck />}
icon={<RadioIcon />}
/>
) : (
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
)
}
label={variant.answer}
onClick={(event) => {
event.preventDefault();
const variantId = currentQuestion.content.variants[index].id;
if (currentQuestion.content.multi) {
const currentAnswer =
typeof answer !== "string" ? answer || [] : [];
updateAnswer(
currentQuestion.content.id,
currentAnswer?.includes(variantId)
? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId]
);
return;
}
updateAnswer(currentQuestion.content.id, variantId);
if (answer === variantId) {
deleteAnswer(currentQuestion.content.id);
}
}}
currentQuestion={currentQuestion}
variant={variant}
answer={answer}
index={index}
/>
))}
{currentQuestion.content.own && ownVariant && (
<VariantItem
own
currentQuestion={currentQuestion}
variant={ownVariant.variant}
answer={answer}
index={currentQuestion.content.variants.length + 2}
/>
)}
</Box>
</Group>
{currentQuestion.content.back && (
@ -126,3 +117,70 @@ export const Variant = ({ currentQuestion }: VariantProps) => {
</Box>
);
};
const VariantItem = ({
currentQuestion,
variant,
answer,
index,
own = false,
}: VariantItemProps) => {
const theme = useTheme();
return (
<FormControlLabel
key={variant.id}
sx={{
margin: "0",
borderRadius: "12px",
padding: "15px",
border: `1px solid ${theme.palette.grey2.main}`,
display: "flex",
maxWidth: "685px",
justifyContent: "space-between",
width: "100%",
"&.MuiFormControl-root": {
width: "100%",
}
}}
value={index}
labelPlacement="start"
control={
currentQuestion.content.multi ? (
<Checkbox
checked={!!answer?.includes(variant.id)}
checkedIcon={<CheckboxIcon checked />}
icon={<CheckboxIcon />}
/>
) : (
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
)
}
label={own ? <TextField label="Другое..." /> : variant.answer}
onClick={(event) => {
event.preventDefault();
const variantId = currentQuestion.content.variants[index].id;
if (currentQuestion.content.multi) {
const currentAnswer = typeof answer !== "string" ? answer || [] : [];
updateAnswer(
currentQuestion.content.id,
currentAnswer.includes(variantId)
? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId]
);
return;
}
updateAnswer(currentQuestion.content.id, variantId);
if (answer === variantId) {
deleteAnswer(currentQuestion.content.id);
}
}}
/>
);
};

@ -7,6 +7,8 @@ import {
useTheme,
} from "@mui/material";
import gag from "./gag.png"
import { useQuizViewStore, updateAnswer, deleteAnswer } from "@root/quizView";
import RadioCheck from "@ui_kit/RadioCheck";
@ -29,6 +31,8 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
({ id }) => answer === id
);
console.log(currentQuestion)
return (
<Box>
<Typography variant="h5">{currentQuestion.title}</Typography>
@ -79,26 +83,37 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
))}
</Box>
</RadioGroup>
{(variant?.extendedText || currentQuestion.content.back) && (
{/* {(variant?.extendedText || currentQuestion.content.back) && ( */}
<Box
sx={{
maxWidth: "450px",
width: "100%",
height: "450px",
border: "1px solid #E3E3E3",
border: "1px solid #9A9AAF",
borderRadius: "12px",
overflow: "hidden",
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#9A9AAF12",
color: "#9A9AAF"
}}
>
<img
{
answer ?
<img
src={
answer ? variant?.extendedText : currentQuestion.content.back
variant?.extendedText || gag
}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>
:
(variant?.extendedText || "Выберите вариант ответа слева")
}
</Box>
)}
{/* )} */}
</Box>
</Box>
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

@ -1,14 +1,14 @@
import { login } from "@api/auth";
import CloseIcon from "@mui/icons-material/Close";
import {
Box,
Button,
Dialog,
IconButton,
Link,
Typography,
useMediaQuery,
useTheme,
Box,
Button,
Dialog,
IconButton,
Link,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { setUserId, useUserStore } from "@root/user";
import InputTextfield from "@ui_kit/InputTextfield";
@ -17,7 +17,7 @@ import PasswordInput from "@ui_kit/passwordInput";
import { useFormik } from "formik";
import { enqueueSnackbar } from "notistack";
import { useEffect, useState } from "react";
import { Link as RouterLink, useNavigate } from "react-router-dom";
import { Link as RouterLink, useNavigate, useLocation } from "react-router-dom";
import { object, string } from "yup";
interface Values {
@ -43,6 +43,8 @@ export default function SigninDialog() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const navigate = useNavigate();
const location = useLocation();
const formik = useFormik<Values>({
initialValues,
validationSchema,
@ -110,11 +112,11 @@ export default function SigninDialog() {
borderRadius: "12px",
boxShadow: "0px 15px 80px rgb(210 208 225 / 70%)",
"& .MuiFormHelperText-root.Mui-error, & .MuiFormHelperText-root.Mui-error.MuiFormHelperText-filled":
{
position: "absolute",
top: "46px",
margin: "0",
},
{
position: "absolute",
top: "46px",
margin: "0",
},
}}
>
<IconButton
@ -153,7 +155,7 @@ export default function SigninDialog() {
id="email"
label="Email"
gap={upMd ? "10px" : "10px"}
/>
/>
<PasswordInput
TextfieldProps={{
value: formik.values.password,
@ -190,16 +192,17 @@ export default function SigninDialog() {
Войти
</Button>
{/* <Link
component={RouterLink}
to="/"
href="#"
sx={{
color: "#4D4D4D",
mb: "15px",
}}
>
Забыли пароль?
</Link> */}
component={RouterLink}
to="/"
href="#"
sx={{
color: "#4D4D4D",
mb: "15px",
}}
>
Забыли пароль?
</Link> */}
<Box
sx={{
display: "flex",

@ -17,7 +17,7 @@ import PasswordInput from "@ui_kit/passwordInput";
import { useFormik } from "formik";
import { enqueueSnackbar } from "notistack";
import { useEffect, useState } from "react";
import { Link as RouterLink, useNavigate } from "react-router-dom";
import {Link as RouterLink, useLocation, useNavigate} from "react-router-dom";
import { object, ref, string } from "yup";
interface Values {
@ -50,6 +50,8 @@ export default function SignupDialog() {
const user = useUserStore((state) => state.user);
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const location = useLocation()
const navigate = useNavigate();
const formik = useFormik<Values>({
initialValues,
@ -220,6 +222,7 @@ export default function SignupDialog() {
<Link
component={RouterLink}
to="/signin"
state={{ backgroundLocation: location.state.backgroundLocation }}
sx={{
color: "#7E2AEA",
mt: "auto",

@ -1,5 +1,3 @@
import { quizApi } from "@api/quiz";
import { devlog } from "@frontend/kitui";
import {
Box,
Button,
@ -9,17 +7,14 @@ import {
useMediaQuery,
useTheme,
} from "@mui/material";
import { createQuiz } from "@root/quizes/actions";
import { useQuizes } from "@root/quizes/hooks";
import SectionWrapper from "@ui_kit/SectionWrapper";
import { isAxiosError } from "axios";
import { enqueueSnackbar } from "notistack";
import React from "react";
import { useNavigate } from "react-router-dom";
import useSWR from "swr";
import ComplexNavText from "./ComplexNavText";
import FirstQuiz from "./FirstQuiz";
import QuizCard from "./QuizCard";
import { setQuizes, createQuiz } from "@root/quizes/actions";
import { useQuizStore } from "@root/quizes/store";
interface Props {
@ -31,23 +26,14 @@ export default function MyQuizzesFull({
outerContainerSx: sx,
children,
}: Props) {
useSWR("quizes", () => quizApi.getList(), {
onSuccess: setQuizes,
onError: error => {
const message = isAxiosError<string>(error) ? (error.response?.data ?? "") : "";
devlog("Error getting quiz list", error);
enqueueSnackbar(`Не удалось получить квизы. ${message}`);
},
});
const quizArray = useQuizStore(state => state.quizes);
const { quizes } = useQuizes();
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(500));
return (
<>
{quizArray.length === 0 ? (
{quizes.length === 0 ? (
<FirstQuiz />
) : (
<SectionWrapper maxWidth="lg">
@ -83,7 +69,7 @@ export default function MyQuizzesFull({
mb: "60px",
}}
>
{quizArray.map(quiz => (
{quizes.map(quiz => (
<QuizCard
key={quiz.id}
quiz={quiz}

@ -29,36 +29,41 @@ import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import useSWR from "swr";
import { SidebarMobile } from "./Sidebar/SidebarMobile";
import {cleanQuestions, updateOpenBranchingPanel} from "@root/questions/actions";
import {BranchingPanel} from "../Questions/BranchingPanel";
import {useQuestionsStore} from "@root/questions/store";
import { useQuestions } from "@root/questions/hooks";
import { cleanQuestions } from "@root/questions/actions";
import { updateOpenBranchingPanel } from "@root/uiTools/actions";
import { BranchingPanel } from "../Questions/BranchingPanel";
import { setQuestions } from "@root/questions/actions";
import { useQuestionsStore } from "@root/questions/store";
import { useQuizes } from "@root/quizes/hooks";
import { questionApi } from "@api/question";
import { useUiTools } from "@root/uiTools/store";
export default function EditPage() {
useSWR("quizes", () => quizApi.getList(), {
onSuccess: setQuizes,
onError: error => {
const message = isAxiosError<string>(error) ? (error.response?.data ?? "") : "";
const quiz = useCurrentQuiz();
const { editQuizId } = useQuizStore();
devlog("Error getting quiz list", error);
enqueueSnackbar(`Не удалось получить квизы. ${message}`);
},
});
useEffect(() => {
const getData = async () => {
const quizes = await quizApi.getList()
setQuizes(quizes)
// if (isLoading && !questions) return <Box>Загрузка вопросов...</Box>;
const questions = await questionApi.getList({ quiz_id: editQuizId })
setQuestions(questions)
}
getData()
}, [])
const { openBranchingPanel } = useUiTools()
const theme = useTheme();
const navigate = useNavigate();
const editQuizId = useQuizStore(state => state.editQuizId);
const quiz = useCurrentQuiz();
const currentStep = useQuizStore(state => state.currentStep);
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isMobile = useMediaQuery(theme.breakpoints.down(660));
const [mobileSidebar, setMobileSidebar] = useState<boolean>(false);
const {openBranchingPanel} = useQuestionsStore.getState()
const quizConfig = quiz?.config;
const { questions, isLoading } = useQuestions();
useEffect(() => {
if (editQuizId === null) navigate("/list");
@ -69,6 +74,7 @@ export default function EditPage() {
cleanQuestions();
}, []);
return (
<>
{/*хедер*/}
@ -213,7 +219,7 @@ export default function EditPage() {
sx={{
background: theme.palette.background.default,
width: "100%",
padding: isMobile ? "16px" : "25px",
padding: isMobile ? "16px 16px 140px 16px" : "25px",
height: "calc(100vh - 80px)",
overflow: "auto",
boxSizing: "border-box",
@ -232,7 +238,7 @@ export default function EditPage() {
</>
}
</Box>
{isTablet && [1, 2, 3].includes(currentStep) && (
{isTablet &&
<Box
sx={{
position: "absolute",
@ -246,75 +252,74 @@ export default function EditPage() {
background: "#FFF",
}}
>
<Box
sx={{
display: openBranchingPanel ? "none" : "display",
alignItems: "center",
gap: "15px",
padding: "18px",
background: "#fff",
borderRadius: "12px",
boxShadow: "0px 10px 30px #e7e7e7",
}}
>
<Switch
value={openBranchingPanel}
onChange={(_, value) => {
updateOpenBranchingPanel(value)
}}
{[1, 2].includes(currentStep) && !openBranchingPanel && (
<Box
sx={{
width: 50,
height: 30,
padding: 0,
"& .MuiSwitch-switchBase": {
padding: 0,
margin: "2px",
transitionDuration: "300ms",
"&.Mui-checked": {
transform: "translateX(20px)",
color: theme.palette.brightPurple.main,
"& + .MuiSwitch-track": {
backgroundColor: "#E8DCF9",
opacity: 1,
border: 0,
},
"&.Mui-disabled + .MuiSwitch-track": { opacity: 0.5 },
},
"&.Mui-disabled .MuiSwitch-thumb": {
color:
theme.palette.mode === "light"
? theme.palette.grey[100]
: theme.palette.grey[600],
},
"&.Mui-disabled + .MuiSwitch-track": {
opacity: theme.palette.mode === "light" ? 0.7 : 0.3,
},
},
"& .MuiSwitch-thumb": {
boxSizing: "border-box",
width: 25,
height: 25,
},
"& .MuiSwitch-track": {
borderRadius: 13,
backgroundColor:
theme.palette.mode === "light" ? "#E9E9EA" : "#39393D",
opacity: 1,
transition: theme.transitions.create(["background-color"], {
duration: 500,
}),
},
display: "flex",
alignItems: "center",
gap: "15px",
padding: "18px",
background: "#fff",
borderRadius: "12px",
boxShadow: "0px 10px 30px #e7e7e7",
}}
/>
<Box>
<Typography sx={{ fontWeight: "bold", color: "#4D4D4D" }}>
Логика ветвления
</Typography>
<Typography sx={{ color: "#4D4D4D", fontSize: "12px" }}>
Настройте связи между вопросами
</Typography>
>
<Switch
checked={openBranchingPanel}
onChange={
(e) => updateOpenBranchingPanel(e.target.checked)
}
sx={{
width: 50,
height: 30,
padding: 0,
"& .MuiSwitch-switchBase": {
padding: 0,
margin: "2px",
transitionDuration: "300ms",
"&.Mui-checked": {
transform: "translateX(20px)",
color: theme.palette.brightPurple.main,
"& + .MuiSwitch-track": {
backgroundColor: "#E8DCF9",
opacity: 1,
border: 0,
},
"&.Mui-disabled + .MuiSwitch-track": { opacity: 0.5 },
},
"&.Mui-disabled .MuiSwitch-thumb": {
color:
theme.palette.mode === "light"
? theme.palette.grey[100]
: theme.palette.grey[600],
},
"&.Mui-disabled + .MuiSwitch-track": {
opacity: theme.palette.mode === "light" ? 0.7 : 0.3,
},
},
"& .MuiSwitch-thumb": {
boxSizing: "border-box",
width: 25,
height: 25,
},
"& .MuiSwitch-track": {
borderRadius: 13,
backgroundColor:
theme.palette.mode === "light" ? "#E9E9EA" : "#39393D",
opacity: 1,
transition: theme.transitions.create(["background-color"], {
duration: 500,
}),
},
}}
/>
<Box>
<Typography sx={{ fontWeight: "bold", color: "#4D4D4D" }}>
Логика ветвления
</Typography>
</Box>
</Box>
</Box>
)}
<Button
variant="contained"
sx={{
@ -326,7 +331,8 @@ export default function EditPage() {
Опубликовать
</Button>
</Box>
)}
}
</Box>
</>
);

@ -331,74 +331,7 @@ export default function StartPageSettings() {
<ModalSizeImage />
<Box
sx={{
mt: "10px",
display: "flex",
gap: "10px",
flexDirection: "column",
}}
>
<FormControlLabel
control={
<Checkbox
icon={<IconCheck />}
checkedIcon={<MobilePhoneIcon bgcolor={"#EEE4FC"} />}
/>
}
label="мобильная версия"
sx={{
color: theme.palette.brightPurple.main,
textDecorationLine: "underline",
textDecorationColor: theme.palette.brightPurple.main,
ml: "-9px",
userSelect: "none",
"& .css-14o5ia4-MuiTypography-root": {
fontSize: "16px"
}
}}
onClick={() => {
MobileVersionHC(!mobileVersion);
}}
/>
{mobileVersion ? (
<Box sx={{ display: "flex", flexDirection: "column" }}>
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
mt: "11px",
mb: "14px",
}}
>
Изображение для мобильной версии
</Typography>
<DropZone
text={"5 MB максимум"}
imageUrl={quiz.config.startpage.background.mobile}
originalImageUrl={quiz.config.startpage.background.originalMobile}
onImageUploadClick={file => {
uploadQuizImage(quiz.id, file, (quiz, url) => {
quiz.config.startpage.background.mobile = url;
quiz.config.startpage.background.originalMobile = url;
});
}}
onImageSaveClick={file => {
uploadQuizImage(quiz.id, file, (quiz, url) => {
quiz.config.startpage.background.mobile = url;
});
}}
onDeleteClick={() => {
updateQuiz(quiz.id, quiz => {
quiz.config.startpage.background.mobile = null;
});
}}
/>
</Box>
) : (
<></>
)}
</Box>
</Box>
<Box
@ -415,96 +348,52 @@ export default function StartPageSettings() {
quiz.config.startpage.background.video = e.target.value;
})}
/>
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
mt: "20px",
mb: "5px",
}}
>
Настройки видео
</Typography>
<CustomCheckbox
label="Зацикливать видео"
checked={quiz.config.startpage.background.cycle}
handleChange={e => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.background.cycle = e.target.checked;
})}
/>
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
mt: "11px",
mb: "14px",
}}
>
Изображение для мобильной версии
</Typography>
<DropZone
text={"5 MB максимум"}
imageUrl={quiz.config.startpage.background.mobile}
originalImageUrl={quiz.config.startpage.background.originalMobile}
onImageUploadClick={file => {
uploadQuizImage(quiz.id, file, (quiz, url) => {
quiz.config.startpage.background.mobile = url;
quiz.config.startpage.background.originalMobile = url;
});
}}
onImageSaveClick={file => {
uploadQuizImage(quiz.id, file, (quiz, url) => {
quiz.config.startpage.background.mobile = url;
});
}}
onDeleteClick={() => {
updateQuiz(quiz.id, quiz => {
quiz.config.startpage.background.mobile = null;
});
}}
/>
</Box>
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
mt: "20px",
mb: "14px",
}}
>
Расположение элементов
</Typography>
{designType !== "centered" &&
<Box
sx={{
display: "flex",
gap: "10px",
}}
>
<SelectableIconButton
onClick={() => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.position = "left";
})}
isActive={quiz.config.startpage.position === "left"}
Icon={AlignLeftIcon}
/>
<SelectableIconButton
onClick={() => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.position = "center";
})}
isActive={quiz.config.startpage.position === "center"}
Icon={AlignCenterIcon}
sx={{ display: designType === "standard" ? "none" : "flex" }}
/>
<SelectableIconButton
onClick={() => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.position = "right";
})}
isActive={quiz.config.startpage.position === "right"}
Icon={AlignRightIcon}
/>
</Box>
<>
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
mt: "20px",
mb: "14px",
}}
>
Расположение элементов
</Typography>
<Box
sx={{
display: "flex",
gap: "10px",
}}
>
<SelectableIconButton
onClick={() => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.position = "left";
})}
isActive={quiz.config.startpage.position === "left"}
Icon={AlignLeftIcon}
/>
<SelectableIconButton
onClick={() => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.position = "center";
})}
isActive={quiz.config.startpage.position === "center"}
Icon={AlignCenterIcon}
sx={{ display: designType === "standard" ? "none" : "flex" }}
/>
<SelectableIconButton
onClick={() => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.position = "right";
})}
isActive={quiz.config.startpage.position === "right"}
Icon={AlignRightIcon}
/>
</Box>
</>
}
{(isTablet || !isSmallMonitor) && (
<>

@ -99,7 +99,6 @@ const updateQuestionOrders = () => {
const questions = useQuestionsStore.getState().questions.filter(
(question): question is AnyTypedQuizQuestion => question.type !== null && question.type !== "result"
);
console.log(questions);
questions.forEach((question, index) => {
updateQuestion(question.id, question => {
@ -176,9 +175,9 @@ const REQUEST_DEBOUNCE = 200;
const requestQueue = new RequestQueue();
let requestTimeoutId: ReturnType<typeof setTimeout>;
export const updateQuestion = (
export const updateQuestion = <T = AnyTypedQuizQuestion>(
questionId: string,
updateFn: (question: AnyTypedQuizQuestion) => void,
updateFn: (question: T) => void,
skipQueue = false,
) => {
setProducedState(state => {
@ -186,7 +185,7 @@ export const updateQuestion = (
if (!question) return;
if (question.type === null) throw new Error("Cannot update untyped question, use 'updateUntypedQuestion' instead");
updateFn(question);
updateFn(question as T);
}, {
type: "updateQuestion",
questionId,
@ -206,8 +205,10 @@ export const updateQuestion = (
//Если мы делаем листочек веточкой - удаляем созданный к нему результ
const questionResult = useQuestionsStore.getState().questions.find(questionResult => questionResult.type === "result" && questionResult.content.rule.parentId === q.content.id);
if (questionResult && q.content.rule.default.length !== 0) deleteQuestion(questionResult.quizId);
deleteQuestion;
setQuestionBackendId(questionId, response.updated);
if (q.backendId !== response.updated) {
console.warn(`Question backend id has changed from ${q.backendId} to ${response.updated}`);
}
} catch (error) {
if (isAxiosCanceledError(error)) return;
@ -384,13 +385,16 @@ export const createTypedQuestion = async (
});
export const deleteQuestion = async (questionId: string) => requestQueue.enqueue(async () => {
console.log("Я получил запрос на удаление. ИД - ", questionId)
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
console.log("delete question ", question)
if (!question) return;
if (question.type === null) {
console.log("removeQuestion")
removeQuestion(questionId);
return;
}
@ -415,7 +419,6 @@ export const copyQuestion = async (questionId: string, quizId: number) => reques
if (question.type === null) {
const copiedQuestion = structuredClone(question);
copiedQuestion.id = frontId;
copiedQuestion.content.id = frontId;
setProducedState(state => {
state.questions.push(copiedQuestion);
@ -460,9 +463,7 @@ function setProducedState<A extends string | { type: unknown; }>(
};
export const cleardragQuestionContentId = () => {
useQuestionsStore.setState({ dragQuestionContentId: null });
};
export const getQuestionById = (questionId: string | null) => {
if (questionId === null) return null;
@ -478,9 +479,7 @@ export const getQuestionByContentId = (questionContentId: string | null) => {
};
export const updateOpenedModalSettingsId = (id?: string) => useQuestionsStore.setState({ openedModalSettingsId: id ? id : null });
export const updateDragQuestionContentId = (contentId?: string) => {
useQuestionsStore.setState({ dragQuestionContentId: contentId ? contentId : null });
};
export const clearRuleForAll = () => {
const { questions } = useQuestionsStore.getState();
@ -495,23 +494,6 @@ export const clearRuleForAll = () => {
});
};
export const updateOpenBranchingPanel = (value: boolean) => useQuestionsStore.setState({ openBranchingPanel: value });
let UDTOABM: ReturnType<typeof setTimeout>;
export const updateDesireToOpenABranchingModal = (contentId: string) => {
useQuestionsStore.setState({ desireToOpenABranchingModal: contentId });
clearTimeout(UDTOABM);
UDTOABM = setTimeout(() => {
useQuestionsStore.setState({ desireToOpenABranchingModal: null });
}, 7000);
};
export const clearDesireToOpenABranchingModal = () => {
useQuestionsStore.setState({ desireToOpenABranchingModal: null });
};
export const updateEditSomeQuestion = (contentId?: string) => {
useQuestionsStore.setState({ editSomeQuestion: contentId === undefined ? null : contentId });
};
export const createFrontResult = (quizId: number, parentContentId?: string) => setProducedState(state => {
const frontId = nanoid();

@ -6,6 +6,7 @@ import useSWR from "swr";
import { setQuestions } from "./actions";
import { useQuestionsStore } from "./store";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { useEffect } from "react";
export function useQuestions() {

@ -5,20 +5,10 @@ import { devtools } from "zustand/middleware";
export type QuestionsStore = {
questions: (AnyTypedQuizQuestion | UntypedQuizQuestion)[];
openedModalSettingsId: string | null;
dragQuestionContentId: string | null;
openBranchingPanel: boolean;
desireToOpenABranchingModal: string | null;
editSomeQuestion: string | null;
};
const initialState: QuestionsStore = {
questions: [],
openedModalSettingsId: null as null,
dragQuestionContentId: null,
openBranchingPanel: false,
desireToOpenABranchingModal: null as null,
editSomeQuestion: null as null,
};

@ -1,21 +1,28 @@
import { create } from "zustand";
import { devtools } from "zustand/middleware";
import type { QuestionVariant } from "../model/questionTypes/shared";
type Answer = {
questionId: string;
answer: string | string[];
// Поле отвечающее за первое изменение ответа, нужно для галочки "Необязательный вопрос"
changed: boolean;
};
type OwnVariant = {
contentId: string;
variant: QuestionVariant;
};
interface QuizViewStore {
answers: Answer[];
ownVariants: OwnVariant[];
}
export const useQuizViewStore = create<QuizViewStore>()(
devtools(
(set, get) => ({
answers: [],
ownVariants: [],
}),
{
name: "quizView",
@ -23,20 +30,16 @@ export const useQuizViewStore = create<QuizViewStore>()(
)
);
export const updateAnswer = (
questionId: string,
answer: string | string[],
changed = true
) => {
export const updateAnswer = (questionId: string, answer: string | string[]) => {
const answers = [...useQuizViewStore.getState().answers];
const answerIndex = answers.findIndex(
(answer) => questionId === answer.questionId
);
if (answerIndex < 0) {
answers.push({ questionId, answer, changed });
answers.push({ questionId, answer });
} else {
answers[answerIndex] = { questionId, answer, changed };
answers[answerIndex] = { questionId, answer };
}
useQuizViewStore.setState({ answers });
@ -50,3 +53,44 @@ export const deleteAnswer = (questionId: string) => {
useQuizViewStore.setState({ answers: filteredItems });
};
export const updateOwnVariant = (contentId: string, answer: string) => {
const ownVariants = [...useQuizViewStore.getState().ownVariants];
const ownVariantIndex = ownVariants.findIndex(
(variant) => variant.contentId === contentId
);
if (ownVariantIndex < 0) {
ownVariants.push({
contentId,
variant: {
id: getRandom(),
answer,
extendedText: "",
hints: "",
originalImageUrl: "",
},
});
} else {
ownVariants[ownVariantIndex].variant.answer = answer;
}
useQuizViewStore.setState({ ownVariants });
};
export const deleteOwnVariant = (contentId: string) => {
const ownVariants = [...useQuizViewStore.getState().ownVariants];
const filteredOwnVariants = ownVariants.filter(
(variant) => variant.contentId !== contentId
);
useQuizViewStore.setState({ ownVariants: filteredOwnVariants });
};
function getRandom() {
const min = Math.ceil(1000000);
const max = Math.floor(10000000);
return String(Math.floor(Math.random() * (max - min)) + min);
}

@ -179,6 +179,7 @@ export const deleteQuiz = async (quizId: string) => requestQueue.enqueue(async (
export const updateRootContentId = (quizId: string, id:string) => updateQuiz(
quizId,
quiz => {
console.log("Я изменение статуса корня проекта дерева, меняю на ", id)
quiz.config.haveRoot = id
},
);

@ -1,11 +1,34 @@
import useSWR from "swr";
import { useQuizStore } from "./store";
import { quizApi } from "@api/quiz";
import { setQuizes } from "./actions";
import { isAxiosError } from "axios";
import { devlog } from "@frontend/kitui";
import { enqueueSnackbar } from "notistack";
export function useCurrentQuiz() {
const quizId = useQuizStore(state => state.editQuizId);
export function useQuizes() {
const { isLoading, error, isValidating } = useSWR("quizes", () => quizApi.getList(), {
onSuccess: setQuizes,
onError: (error: unknown) => {
const message = isAxiosError<string>(error)
? error.response?.data ?? ""
: "";
devlog("Error getting quiz list", error);
enqueueSnackbar("Не удалось получить квизы");
},
});
const quizes = useQuizStore(state => state.quizes);
const quiz = quizes.find(q => q.backendId === quizId);
return { quizes, isLoading, error, isValidating };
}
export function useCurrentQuiz() {
const { quizes, editQuizId } = useQuizStore();
const quiz = quizes.find(q => q.backendId === editQuizId);
return quiz;
}

@ -0,0 +1,31 @@
import { useUiTools } from "./store";
export const updateOpenBranchingPanel = (value: boolean) => useUiTools.setState({ openBranchingPanel: value });
export const cleardragQuestionContentId = () => {
useUiTools.setState({ dragQuestionContentId: null });
};
export const updateDragQuestionContentId = (contentId?: string) => {
useUiTools.setState({ dragQuestionContentId: contentId ? contentId : null });
};
let UDTOABM: ReturnType<typeof setTimeout>;
export const updateDesireToOpenABranchingModal = (contentId: string) => {
useUiTools.setState({ desireToOpenABranchingModal: contentId });
clearTimeout(UDTOABM);
UDTOABM = setTimeout(() => {
useUiTools.setState({ desireToOpenABranchingModal: null });
}, 7000);
};
export const clearDesireToOpenABranchingModal = () => {
useUiTools.setState({ desireToOpenABranchingModal: null });
};
export const updateEditSomeQuestion = (contentId?: string) => {
useUiTools.setState({ editSomeQuestion: contentId === undefined ? null : contentId });
};

@ -0,0 +1,30 @@
import { create } from "zustand";
import { devtools } from "zustand/middleware";
export type UiTools = {
openedModalSettingsId: string | null;
dragQuestionContentId: string | null;
openBranchingPanel: boolean;
desireToOpenABranchingModal: string | null;
editSomeQuestion: string | null;
};
const initialState: UiTools = {
openedModalSettingsId: null as null,
dragQuestionContentId: null,
openBranchingPanel: false,
desireToOpenABranchingModal: null as null,
editSomeQuestion: null as null,
};
export const useUiTools = create<UiTools>()(
devtools(
() => initialState,
{
name: "UiTools",
enabled: process.env.NODE_ENV === "development",
trace: process.env.NODE_ENV === "development",
}
)
);

@ -1,6 +1,8 @@
import { FormControlLabel, Checkbox, useTheme, Box, useMediaQuery } from "@mui/material";
import React from "react";
import { CheckboxIcon } from "@icons/Checkbox";
import type { SxProps } from "@mui/material";
interface Props {
@ -21,8 +23,8 @@ export default function CustomCheckbox({ label, handleChange, checked, sx, dataC
<Checkbox
sx={{ padding: "0px 13px 1px 11px" }}
disableRipple
icon={<Icon />}
checkedIcon={<CheckedIcon />}
icon={<CheckboxIcon />}
checkedIcon={<CheckboxIcon checked />}
onChange={handleChange}
checked={checked}
data-cy={dataCy}
@ -37,42 +39,3 @@ export default function CustomCheckbox({ label, handleChange, checked, sx, dataC
/>
);
}
function Icon() {
const theme = useTheme();
return (
<Box
sx={{
height: "24px",
width: "24px",
borderRadius: "6px",
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.grey2.main}`,
}}
/>
);
}
function CheckedIcon() {
const theme = useTheme();
return (
<Box
sx={{
height: "24px",
width: "24px",
borderRadius: "6px",
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: theme.palette.brightPurple.main,
border: `1px solid ${theme.palette.grey2.main}`,
}}
>
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 25 18" fill="none">
<path d="M2 9L10 16.5L22.5 1.5" stroke="#ffffff" strokeWidth="4" strokeLinecap="round" />
</svg>
</Box>
);
}

@ -1,12 +1,13 @@
import { Box } from "@mui/material";
import { Box, SxProps } from "@mui/material";
interface Props {
videoUrl: string;
containerSX?: SxProps;
}
export default function YoutubeEmbedIframe({ videoUrl }: Props) {
export default function YoutubeEmbedIframe({ videoUrl, containerSX }: Props) {
const extractYoutubeVideoId = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/gi;
const videoId = extractYoutubeVideoId.exec(videoUrl)?.[1];
if (!videoId) return null;
@ -21,7 +22,8 @@ export default function YoutubeEmbedIframe({ videoUrl }: Props) {
"& iframe": {
width: "100%",
height: "100%",
}
},
...containerSX
}}>
<iframe
src={embedUrl}

@ -1034,7 +1034,7 @@
core-js-pure "^3.25.1"
regenerator-runtime "^0.13.11"
"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.9", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.1", "@babel/runtime@^7.23.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.9", "@babel/runtime@^7.23.1", "@babel/runtime@^7.23.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
version "7.23.2"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz"
integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==
@ -1868,7 +1868,7 @@
schema-utils "^3.0.0"
source-map "^0.7.3"
"@popperjs/core@^2.0.0", "@popperjs/core@^2.11.8", "@popperjs/core@^2.9.2":
"@popperjs/core@^2.0.0", "@popperjs/core@^2.11.8":
version "2.11.8"
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
@ -2412,16 +2412,6 @@
"@types/cytoscape" "*"
"@types/react" "*"
"@types/react-datepicker@^4.19.3":
version "4.19.3"
resolved "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-4.19.3.tgz"
integrity sha512-85F9eKWu9fGiD9r4KVVMPYAdkJJswR3Wci9PvqplmB6T+D+VbUqPeKtifg96NZ4nEhufjehW+SX4JLrEWVplWw==
dependencies:
"@popperjs/core" "^2.9.2"
"@types/react" "*"
date-fns "^2.0.1"
react-popper "^2.2.5"
"@types/react-dnd@^3.0.2":
version "3.0.2"
resolved "https://registry.npmjs.org/@types/react-dnd/-/react-dnd-3.0.2.tgz"
@ -3610,11 +3600,6 @@ cjs-module-lexer@^1.0.0:
resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz"
integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==
classnames@^2.2.6:
version "2.3.2"
resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz"
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
clean-css@^5.2.2:
version "5.3.1"
resolved "https://registry.npmjs.org/clean-css/-/clean-css-5.3.1.tgz"
@ -4214,13 +4199,6 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
date-fns@^2.0.1, date-fns@^2.30.0:
version "2.30.0"
resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz"
integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
dependencies:
"@babel/runtime" "^7.21.0"
dayjs@^1.10.4, dayjs@^1.11.10:
version "1.11.10"
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz"
@ -7115,7 +7093,7 @@ log-update@^4.0.0:
slice-ansi "^4.0.0"
wrap-ansi "^6.2.0"
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@ -8544,18 +8522,6 @@ react-cytoscapejs@^2.0.0:
dependencies:
prop-types "^15.8.1"
react-datepicker@^4.24.0:
version "4.24.0"
resolved "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.24.0.tgz"
integrity sha512-2QUC2pP+x4v3Jp06gnFllxKsJR0yoT/K6y86ItxEsveTXUpsx+NBkChWXjU0JsGx/PL8EQnsxN0wHl4zdA1m/g==
dependencies:
"@popperjs/core" "^2.11.8"
classnames "^2.2.6"
date-fns "^2.30.0"
prop-types "^15.7.2"
react-onclickoutside "^6.13.0"
react-popper "^2.3.0"
react-dev-utils@^12.0.1:
version "12.0.1"
resolved "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz"
@ -8645,11 +8611,6 @@ react-fast-compare@^2.0.1:
resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
react-fast-compare@^3.0.1:
version "3.2.2"
resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz"
integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==
react-image-crop@^10.1.5:
version "10.1.8"
resolved "https://registry.npmjs.org/react-image-crop/-/react-image-crop-10.1.8.tgz"
@ -8675,19 +8636,6 @@ react-is@^18.0.0, react-is@^18.2.0:
resolved "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
react-onclickoutside@^6.13.0:
version "6.13.0"
resolved "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz"
integrity sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==
react-popper@^2.2.5, react-popper@^2.3.0:
version "2.3.0"
resolved "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz"
integrity sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==
dependencies:
react-fast-compare "^3.0.1"
warning "^4.0.2"
react-redux@^7.2.0:
version "7.2.9"
resolved "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz"
@ -10260,13 +10208,6 @@ walker@^1.0.7:
dependencies:
makeerror "1.0.12"
warning@^4.0.2:
version "4.0.3"
resolved "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
dependencies:
loose-envify "^1.0.0"
watchpack@^2.4.0:
version "2.4.0"
resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz"