Merge branch 'staging'

This commit is contained in:
Nastya 2024-11-24 16:53:15 +03:00
commit ab05007b85
113 changed files with 3449 additions and 1321 deletions

2
CHANGELOG.md Normal file

@ -0,0 +1,2 @@
1.0.1 Страница заявок корректно отображает мультиответ
1.0.0 Добавлены фичи "мультиответ", "перенос строки в своём ответе", "свой ответ", "плейсхолдер своего ответа"

@ -3,9 +3,12 @@ include:
file: "/templates/docker/build-template.gitlab-ci.yml"
- project: "devops/pena-continuous-integration"
file: "/templates/docker/deploy-template.gitlab-ci.yml"
- project: "devops/pena-continuous-integration"
file: "/templates/docker/service-discovery.gitlab-ci.yml"
stages:
- build
- deploy
- service-discovery
build-app:
extends: .build_template
tags:
@ -30,3 +33,6 @@ deploy-to-prod:
tags:
- front
- prod
service-discovery:
extends: .sd_artefacts_template

@ -6,6 +6,8 @@ services:
image: $CI_REGISTRY_IMAGE/staging:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
networks:
- marketplace_penahub_frontend
labels:
com.pena.domains: squiz.pena.digital
hostname: squiz
tty: true
networks:

@ -7,7 +7,7 @@
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@frontend/kitui": "^1.0.85",
"@frontend/squzanswerer": "^1.0.55",
"@frontend/squzanswerer": "^1.0.56",
"@mui/icons-material": "^5.10.14",
"@mui/material": "^5.10.14",
"@mui/x-charts": "^6.19.5",
@ -29,7 +29,7 @@
"cytoscape": "^3.26.0",
"cytoscape-popper": "^2.0.0",
"date-fns": "^3.0.6",
"emoji-mart": "^5.5.2",
"emoji-mart": "^5.6.0",
"file-saver": "^2.0.5",
"formik": "^2.4.5",
"html-to-image": "^1.11.11",

@ -0,0 +1,26 @@
import { Box } from "@mui/material";
interface Props {
color?: string;
height?: string;
width?: string;
}
export default function AmoTrash({ color, height, width }: Props) {
return (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.4994 6H4.5" stroke="#FC2012" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.5 18.7492V8.74609H18.5V18.7492C18.5 20.1299 17.3807 21.2492 16 21.2492H8C6.61929 21.2492 5.5 20.1299 5.5 18.7492Z" fill="#FC2012" stroke="#F02B2B"/>
<path d="M15.75 6V4.5C15.75 4.10218 15.592 3.72064 15.3107 3.43934C15.0294 3.15804 14.6478 3 14.25 3H9.75C9.35218 3 8.97064 3.15804 8.68934 3.43934C8.40804 3.72064 8.25 4.10218 8.25 4.5V6" stroke="#FC2012" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</Box>
);
}

@ -0,0 +1,166 @@
import type { PercentCrop } from "react-image-crop";
export const workSpaceTypesList = {
images: {
desktop: {
step: "desktop",
ratio: {
width: 317,
height: 257
}
},
tablet: {
step: "tablet",
ratio: {
width: 455,
height: 257
}
},
mobile: {
step: "mobile",
ratio: {
width: 160,
height: 183
}
},
},
varimg: {
desktop: {
step: "desktop",
ratio: {
width: 450,
height: 450
}
},
mobile: {
step: "mobile",
ratio: {
width: 335,
height: 335,
}
},
},
text: {
desktop: {
step: "desktop",
ratio: {
width: 450,
height: 450
}
},
mobile: {
step: "mobile",
ratio: {
width: 335,
height: 335,
}
},
},
variant: {
desktop: {
step: "desktop",
ratio: {
width: 450,
height: 450
}
},
mobile: {
step: "mobile",
ratio: {
width: 335,
height: 335,
}
},
},
result: {
desktop: {
step: "desktop",
ratio: {
width: 700,
height: 306
}
},
mobile: {
step: "mobile",
ratio: {
width: 335,
height: 236
}
},
}
} as const;
export type WorkSpaceTypesList = typeof workSpaceTypesList;
export type Writable<T> = { -readonly [K in keyof T]: T[K] };
export type CropOnOpenType = {
open: boolean;
originalImageUrl?:string;
imageBlob?: Blob;
editedUrlImagesList?: Record<Partial<ScreenStepsTypes>, string>;
questionId: string;
questionType: AcceptedQuestionTypes;
quizId: string;
variantId?: string;
selfClose: () => void;
setPictureUploading: (is: boolean) => void;
}
export type WorkSpaceTypes = WorkSpaceTypesList[AcceptedQuestionTypes];
export interface CropModalProps {
open: boolean;
editedImages: Record<keyof WorkSpaceTypes, EditedImage>;
workSpaceTypes: WorkSpaceTypes;
originalImageUrl: string;
setEditedImages: (callback: (editedImages: Record<keyof WorkSpaceTypes, EditedImage>) => Record<keyof WorkSpaceTypes, EditedImage>) => void;
onSaveImageClick: () => void;
closeCropModal: CropOnCloseType;
onDeleteClick: () => void;
};
export type AcceptedQuestionTypes = keyof WorkSpaceTypesList; //"images" | "varimg" | "text" | "variant" | "result"
export type CropOnCloseType = () => void;
export type CropOnDeleteIamgeClick = (callback: () => void) => void;
export type EditedImages = Record<Partial<ScreenStepsTypes>, EditedImage>
export type EditedImage = {
step: string,
url: string,
newRules: EditedImageNewRules
}
export type WorkSpaceModel = {
step: Partial<ScreenStepsTypes>,
ratio: CropAspectRatio
};
export type CropAspectRatio = {
width: number;
height: number;
};
export type ScreenStepsTypes = "desktop" | "tablet" | "mobile" | "small";
export type EditedImageNewRules = {
crop: PercentCrop,
darken: number,
rotate: number,
}
export const DEFAULTCROPRULES = {
crop: {
x: 0,
y: 0,
width: 0,
height: 0,
unit: "%" as "%",
},
rotate: 0,
darken: 0,
}

@ -30,6 +30,17 @@ export function createQuestionVariant(): QuestionVariant {
answer: "",
extendedText: "",
hints: "",
isOwn: false,
originalImageUrl: "",
};
}
export function createQuestionOwnVariant(): QuestionVariant {
return {
id: nanoid(),
answer: "",
extendedText: "",
hints: "",
isOwn: true,
originalImageUrl: "",
};
}

@ -1,16 +1,16 @@
import MiniButtonSetting from "@ui_kit/MiniButtonSetting";
import React from "react";
import SettingIcon from "../../assets/icons/questionsPage/settingIcon";
import Branching from "../../assets/icons/questionsPage/branching";
import SettingIcon from "@/assets/icons/questionsPage/settingIcon";
import Branching from "@/assets/icons/questionsPage/branching";
import { Box, useTheme } from "@mui/material";
import SupplementIcon from "../../assets/icons/ContactFormIcon/supplementIcon";
import SupplementIcon from "@/assets/icons/ContactFormIcon/supplementIcon";
interface Props {
switchState: string;
SSHC: (data: string) => void;
setSwitchState: (data: string) => void;
}
export default function ButtonSettingForms({ SSHC, switchState }: Props) {
export default function ButtonSettingForms({ setSwitchState, switchState }: Props) {
const theme = useTheme();
const buttonSetting: { icon: JSX.Element; title: string; value: string }[] = [
{
@ -68,7 +68,7 @@ export default function ButtonSettingForms({ SSHC, switchState }: Props) {
<MiniButtonSetting
key={i}
onClick={() => {
SSHC(e.value);
setSwitchState(e.value);
}}
sx={{
backgroundColor:

@ -17,19 +17,13 @@ import {
import { incrementCurrentStep, updateQuiz } from "@root/quizes/actions";
import CustomTextField from "@ui_kit/CustomTextField";
import React, { ReactNode, useRef, useState } from "react";
import Info from "../../assets/icons/Info";
import Info from "@/assets/icons/Info";
import Trash from "@icons/trash";
import { OneIcon } from "../../assets/icons/questionsPage/OneIcon";
import AddPlus from "../../assets/icons/questionsPage/addPlus";
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
import ArrowLeft from "@/assets/icons/questionsPage/arrowLeft";
import { decrementCurrentStep } from "@root/quizes/actions";
import ButtonSettingForms from "./ButtonSettingForms";
import DrawerNewField from "./DrawerParent";
import WindowMessengers from "./Massengers/WindowMessengers";
import WindowNewField from "./NewField/WindowNewField";
import SwitchContactForm from "./switchContactForm";
import GearIcon from "@icons/GearIcon";
import { useQuestionsStore } from "@root/questions/store";
import { useCurrentQuiz } from "@root/quizes/hooks";
import {
FieldSettingsDrawerState,

@ -11,16 +11,16 @@ import {
Typography,
useTheme,
} from "@mui/material";
import BrowserIcon from "../../assets/icons/BrowserIcon";
import TiktokIcon from "../../assets/icons/tiktokIcon";
import TelegramIcon from "../../assets/icons/telegramIcon";
import QRIcon from "../../assets/icons/qrIcon";
import BrowserIcon from "@/assets/icons/BrowserIcon";
import TiktokIcon from "@/assets/icons/tiktokIcon";
import TelegramIcon from "@/assets/icons/telegramIcon";
import QRIcon from "@/assets/icons/qrIcon";
import React from "react";
import CustomTextField from "@ui_kit/CustomTextField";
import UploadBox from "@ui_kit/UploadBox";
import UploadIcon from "../../assets/icons/UploadIcon";
import CopyIcon from "../../assets/icons/CopyIcon";
import Qr from "../../assets/Qr.png";
import UploadIcon from "@/assets/icons/UploadIcon";
import CopyIcon from "@/assets/icons/CopyIcon";
import Qr from "@/assets/Qr.png";
export default function ButtonSocial() {
const theme = useTheme();

@ -1,6 +1,6 @@
import { Box, Button } from "@mui/material";
import { decrementCurrentStep } from "@root/quizes/actions";
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
import ArrowLeft from "@/assets/icons/questionsPage/arrowLeft";
import QuizInstallationCard from "./QuizInstallationCard/QuizInstallationCard";
import QuizLinkCard from "./QuizLinkCard";

@ -19,7 +19,7 @@ import PenaTextField from "@ui_kit/PenaTextField";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
import { ReactNode, useState } from "react";
import widgetPreviewImage from "../../../../../assets/widget-preview.png";
import widgetPreviewImage from "@/assets/widget-preview.png";
import WidgetScript from "../../WidgetScript";
import { createBannerWidgetScriptText } from "../../createWidgetScriptText";
import { useWidgetUrl } from "../../useWidgetUrl";

@ -21,8 +21,8 @@ import RadioIcon from "@ui_kit/RadioIcon";
import RunningStripe from "@ui_kit/RunningStripe";
import { nanoid } from "nanoid";
import { ReactNode, useState } from "react";
import Dots from "../../../../../assets/dots.png";
import widgetPreviewImage from "../../../../../assets/widget-preview.png";
import Dots from "@/assets/dots.png";
import widgetPreviewImage from "@/assets/widget-preview.png";
import WidgetScript from "../../WidgetScript";
import { createButtonWidgetScriptText } from "../../createWidgetScriptText";
import { useWidgetUrl } from "../../useWidgetUrl";

@ -5,7 +5,7 @@ import CustomCheckbox from "@ui_kit/CustomCheckbox";
import PenaTextField from "@ui_kit/PenaTextField";
import { nanoid } from "nanoid";
import { ReactNode, useState } from "react";
import Dots from "../../../../assets/dots.png";
import Dots from "@/assets/dots.png";
import WidgetScript from "../WidgetScript";
import { createContainerWidgetScriptText } from "../createWidgetScriptText";
import { useWidgetUrl } from "../useWidgetUrl";

@ -3,7 +3,7 @@ import { useCurrentQuiz } from "@root/quizes/hooks";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
import PenaTextField from "@ui_kit/PenaTextField";
import { ReactNode, useState } from "react";
import Dots from "../../../../assets/dots.png";
import Dots from "@/assets/dots.png";
import WidgetScript from "../WidgetScript";
import { createPopupWidgetScriptText } from "../createWidgetScriptText";
import { useWidgetUrl } from "../useWidgetUrl";

@ -5,7 +5,7 @@ import CircleColorPicker from "@ui_kit/CircleColorPicker";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
import PenaTextField from "@ui_kit/PenaTextField";
import { ReactNode, useState } from "react";
import widgetPreviewImage from "../../../../../assets/widget-preview.png";
import widgetPreviewImage from "@/assets/widget-preview.png";
import WidgetScript from "../../WidgetScript";
import { createSideWidgetScriptText } from "../../createWidgetScriptText";
import { useWidgetUrl } from "../../useWidgetUrl";

@ -15,9 +15,9 @@ import {
import { useCurrentQuiz } from "@root/quizes/hooks";
import { useDomainDefine } from "@utils/hooks/useDomainDefine";
import { FC, useState } from "react";
import ArrowDown from "../../assets/icons/ArrowDownIcon";
import CopyIcon from "../../assets/icons/CopyIcon";
import LinkIcon from "../../assets/icons/LinkIcon";
import ArrowDown from "@/assets/icons/ArrowDownIcon";
import CopyIcon from "@/assets/icons/CopyIcon";
import LinkIcon from "@/assets/icons/LinkIcon";
const TextField = MuiTextField as unknown as FC<TextFieldProps>;

@ -103,9 +103,10 @@ export default function Component() {
<Typography color={"#727074"} fontSize={"14px"}>
ООО Пена © 2024
</Typography>
<TagsCloudBlue sx={{ mt: "10px" }} />
<TagsCloudBlue sx={{mt: "10px"}}/>
</Box>
<Box
sx={{
display: "flex",

@ -2,7 +2,7 @@ import React from "react";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import { Typography, useMediaQuery, useTheme } from "@mui/material";
import abstraction from "../../assets/quizMain.png";
import abstraction from "@/assets/quizMain.png";
import SectionStyled from "./SectionStyled";
import { Link, redirect, useLocation, useNavigate } from "react-router-dom";
import { setIsContactFormOpen } from "@root/contactForm";

@ -4,7 +4,7 @@ import { Fade, Typography, Zoom, useMediaQuery, useTheme } from "@mui/material";
import SectionStyled from "./SectionStyled";
import Link from "@mui/material/Link";
import { styled } from "@mui/material/styles";
import Notebook from "../../assets/LandingPict/notebook";
import Notebook from "@/assets/LandingPict/notebook";
import BigBlock from "./images/bigblock.png";
import businessTasks2 from "./images/businessTasks2.png";
import businessTasks3 from "./images/businessTasks3.png";
@ -15,7 +15,6 @@ import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
// import CalendarW from './image/calendar-W.svg'
// import CalendarP from './image/calendar-P.svg'
import CalendarIcon from "../../assets/LandingPict/calendarIcon";
import { PieСhartIcon } from "@icons/PieСhartIcon";
import { SegmentationIcon } from "@icons/SegmentationIcon";
import { TestingIcon } from "@icons/TestingIcon";

@ -3,7 +3,6 @@ import Box from "@mui/material/Box";
import { SxProps, Typography, useMediaQuery, useTheme } from "@mui/material";
import SectionStyled from "./SectionStyled";
import Button from "@mui/material/Button";
// import Desktop from '../../assets/LandingPict/Desktop.png';
import Desktop1 from "./images/Frame 1171274552.png";
import Desktop2 from "./images/Frame 1171274552-1.png";
import Desktop3 from "./images/Frame 1171274552-2.png";

@ -7,7 +7,7 @@ import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
// import Quotes from './image/quotes.svg';
import { styled } from "@mui/material/styles";
import TitleIcon from "../../assets/LandingPict/titleIcon";
import TitleIcon from "@/assets/LandingPict/titleIcon";
const BoxCard = styled("div")(({ theme }) => ({
[theme.breakpoints.down("md")]: {

@ -10,11 +10,11 @@ import {
useMediaQuery,
useTheme,
} from "@mui/material";
import { addQuestionVariant, deleteQuestionVariant, setQuestionVariantField } from "@root/questions/actions";
import { addQuestionVariant, deleteQuestionVariant, setQuestionVariantField, updateQuestion } from "@root/questions/actions";
import { enqueueSnackbar } from "notistack";
import { memo, type ChangeEvent, type FC, type KeyboardEvent, type ReactNode } from "react";
import { Draggable } from "react-beautiful-dnd";
import type { QuestionVariant } from "@frontend/squzanswerer";
import type { QuestionVariant, QuizQuestionVariant } from "@frontend/squzanswerer";
const TextField = MuiTextField as unknown as FC<TextFieldProps>;
@ -26,13 +26,21 @@ type AnswerItemProps = {
disableKeyDown?: boolean;
additionalContent?: ReactNode;
additionalMobile?: ReactNode;
isOwn: boolean;
ownPlaceholder: string;
};
const AnswerItem = memo<AnswerItemProps>(
({ index, variant, questionId, largeCheck = false, additionalContent, additionalMobile, disableKeyDown }) => {
({ index, variant, questionId, largeCheck = false, additionalContent, additionalMobile, disableKeyDown, isOwn, ownPlaceholder }) => {
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(790));
const setOwnPlaceholder = (replText: string) => {
updateQuestion(questionId, (question) => {
question.content.ownPlaceholder = replText;
});
};
return (
<Draggable
draggableId={String(index)}
@ -55,19 +63,24 @@ const AnswerItem = memo<AnswerItemProps>(
}}
>
<TextField
value={variant.answer}
value={ isOwn ? ownPlaceholder : variant.answer}
fullWidth
focused={false}
placeholder={"Добавьте ответ"}
placeholder={isOwn ? "Добавьте текст-подсказку для ввода “своего ответа”" : "Добавьте ответ"}
multiline={largeCheck}
onChange={({ target }: ChangeEvent<HTMLInputElement>) => {
if (target.value.length <= 1000) {
isOwn ?
setOwnPlaceholder(target.value || " ")
:
setQuestionVariantField(questionId, variant.id, "answer", target.value || " ");
} else {
enqueueSnackbar("Превышена длина вводимого текста")
}
}}
onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
if (disableKeyDown) {
enqueueSnackbar("100 максимальное количество вопросов");
enqueueSnackbar("100 максимальное количество");
} else if (event.code === "Enter" && !largeCheck) {
addQuestionVariant(questionId);
}
@ -88,7 +101,13 @@ const AnswerItem = memo<AnswerItemProps>(
<InputAdornment position="end">
<IconButton
sx={{ padding: "0" }}
onClick={() => deleteQuestionVariant(questionId, variant.id)}
onClick={() => {
isOwn ? updateQuestion<QuizQuestionVariant>(questionId, (question) => {
question.content.own = false;
})
:
deleteQuestionVariant(questionId, variant.id)
}}
>
<DeleteIcon
style={{
@ -104,7 +123,7 @@ const AnswerItem = memo<AnswerItemProps>(
"& .MuiInputBase-root": {
padding: additionalContent ? "5px 13px" : "13px",
borderRadius: "10px",
background: "#ffffff",
background: isOwn ? "#F2F3F7" : "white",
"& input.MuiInputBase-input": {
height: "22px",
},

@ -14,6 +14,8 @@ type Props = Omit<
originalImageUrl?: string | null | undefined,
) => Promise<void>;
openImageUploadModal: () => void;
isOwn: boolean;
ownPlaceholder: string;
};
export default function ImageEditAnswerItem({
@ -27,6 +29,8 @@ export default function ImageEditAnswerItem({
pictureUploding,
openCropModal,
openImageUploadModal,
isOwn,
ownPlaceholder,
}: Props) {
const addOrEditImageButton = useMemo(() => {
return (
@ -105,6 +109,8 @@ export default function ImageEditAnswerItem({
variant={variant}
additionalContent={addOrEditImageButton}
additionalMobile={addOrEditImageButtonMobile}
isOwn={isOwn}
ownPlaceholder={ownPlaceholder}
/>
);
}

@ -38,7 +38,7 @@
align-items: center;
justify-content: center;
border-radius: 6px;
background-image: url("../../../../../assets/icons/ArrowGear.svg");
background-image: url("@/assets/icons/ArrowGear.svg");
font-size: 0px;
background-repeat: no-repeat;
background-size: contain;

@ -1,11 +1,11 @@
import { Box, Skeleton, useMediaQuery, useTheme } from "@mui/material";
import { Box, Skeleton, Typography, useMediaQuery, useTheme } from "@mui/material";
import { deleteTimeoutedQuestions } from "@utils/deleteTimeoutedQuestions";
import { lazy, Suspense, useCallback } from "react";
import { DraggableList } from "./DraggableList";
import { DraggableList } from "../DraggableList";
import { SwitchBranchingPanel } from "./SwitchBranchingPanel";
const BranchingMap = lazy(() =>
import("./Branching/BranchingMap").then((module) => ({ default: module.BranchingMap })),
import("./BranchingMap").then((module) => ({ default: module.BranchingMap })),
);
interface Props {
openBranchingPage: boolean;
@ -51,6 +51,7 @@ export const QuestionSwitchWindowTool = ({
/>
}
>
<Typography fontSize="20px" mb="25px">Настройки ветвления вопросов</Typography>
<BranchingMap />
</Suspense>
) : (

@ -1,145 +0,0 @@
import { devlog } from "@frontend/kitui";
import { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
import { Box, useMediaQuery, useTheme } from "@mui/material";
import { clearRuleForAll } from "@root/questions/actions";
import { useQuestionsStore } from "@root/questions/store";
import { updateRootContentId } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import {
cleardragQuestionContentId,
setModalQuestionParentContentId,
setModalQuestionTargetContentId,
updateOpenedModalSettingsId,
} from "@root/uiTools/actions";
import { useUiTools } from "@root/uiTools/store";
import type { Core } from "cytoscape";
import { enqueueSnackbar } from "notistack";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import CytoscapeComponent from "react-cytoscapejs";
import { withErrorBoundary } from "react-error-boundary";
import { DeleteNodeModal } from "../DeleteNodeModal";
import CsNodeButtons from "./CsNodeButtons";
import { InfoBanner } from "./InfoBanner/InfoBanner";
import { PositionControl } from "./PositionControl/PositionControl";
import { ZoomControl } from "./ZoomControl/ZoomControl";
import { addNode, layoutOptions, storeToNodes } from "./helper";
import { useRemoveNode } from "./hooks/useRemoveNode";
import "./style/styles.css";
import { stylesheet } from "./style/stylesheet";
function CsComponent() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(650));
const [isBannerVisible, setBannerVisible] = useState(true);
const desireToOpenABranchingModal = useUiTools((state) => state.desireToOpenABranchingModal);
const modalQuestionParentContentId = useUiTools((state) => state.modalQuestionParentContentId);
const modalQuestionTargetContentId = useUiTools((state) => state.modalQuestionTargetContentId);
const trashQuestions = useQuestionsStore((state) => state.questions);
const cyRef = useRef<Core | null>(null);
const { removeNode } = useRemoveNode({ cyRef });
const csElements = useMemo(() => {
const questions = trashQuestions.filter(
(question): question is AnyTypedQuizQuestion => question.type !== null && question.type !== "result"
);
return storeToNodes(questions);
}, [trashQuestions]);
useLayoutEffect(() => {
const cy = cyRef?.current;
if (desireToOpenABranchingModal) {
setTimeout(() => {
cy?.getElementById(desireToOpenABranchingModal)?.data("eroticeyeblink", true);
}, 250);
} else {
cy?.elements().data("eroticeyeblink", false);
}
}, [desireToOpenABranchingModal]);
useEffect(() => {
if (modalQuestionTargetContentId.length !== 0 && modalQuestionParentContentId.length !== 0) {
if (!cyRef.current) return;
addNode({
parentNodeContentId: modalQuestionParentContentId,
targetNodeContentId: modalQuestionTargetContentId,
});
}
setModalQuestionParentContentId("");
setModalQuestionTargetContentId("");
}, [modalQuestionTargetContentId]);
useEffect(function onMount() {
updateOpenedModalSettingsId();
document.addEventListener("pointerup", cleardragQuestionContentId);
return () => {
document.removeEventListener("pointerup", cleardragQuestionContentId);
};
}, []);
useEffect(
function rerunLayout() {
cyRef.current?.layout(layoutOptions).run();
cyRef.current?.fit(undefined, 70);
},
[csElements]
);
return (
<Box
sx={{
width: "100%",
}}
>
<CsNodeButtons
csElements={csElements}
cyRef={cyRef}
/>
<Box sx={{ position: "relative" }}>
{isBannerVisible && <InfoBanner setBannerVisible={setBannerVisible} />}
<PositionControl cyRef={cyRef} />
<ZoomControl cyRef={cyRef} />
<CytoscapeComponent
wheelSensitivity={0.1}
elements={csElements}
style={{
height: isMobile ? "327px" : "481px",
background: "#F2F3F7",
overflow: "hidden",
borderRadius: "12px",
width: "100%",
}}
stylesheet={stylesheet}
layout={layoutOptions}
cy={(cy) => {
cyRef.current = cy;
}}
autoungrabify={true}
autounselectify={true}
boxSelectionEnabled={false}
/>
</Box>
<DeleteNodeModal removeNode={removeNode} />
</Box>
);
}
function Clear() {
const quiz = useCurrentQuiz();
if (quiz) {
updateRootContentId(quiz?.id, "");
}
clearRuleForAll();
return <></>;
}
export default withErrorBoundary(CsComponent, {
fallback: <Clear />,
onError: (error, info) => {
enqueueSnackbar("Дерево порвалось");
devlog(info);
devlog(error);
},
});

@ -2,7 +2,7 @@ import { Box, Paper } from "@mui/material";
import { createUntypedQuestion } from "@root/questions/actions";
import { useState } from "react";
import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
import { ReactComponent as PlusIcon } from "../../../assets/icons/plus.svg";
import { ReactComponent as PlusIcon } from "@/assets/icons/plus.svg";
import type { UntypedQuizQuestion } from "../../../model/questionTypes/shared";
import SwitchQuestionsPage from "../SwitchQuestionsPage";
import TypeQuestions from "../TypeQuestions";

@ -42,6 +42,7 @@ import { DeleteFunction } from "@utils/deleteFunc";
import { FC, memo, useRef, useState } from "react";
import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
import { ChooseAnswerModal } from "./ChooseAnswerModal";
import { enqueueSnackbar } from "notistack";
const TextField = MuiTextField as unknown as FC<TextFieldProps>;
@ -124,11 +125,19 @@ const QuestionPageCardTitle = memo<Props>(function ({
id="questionTitle"
value={title}
placeholder={"Заголовок вопроса"}
onChange={({ target }) => setTitle(target.value || " ")}
onChange={({ target }) => {
console.log(target.value.length)
if (target.value.length > maxLengthTextField) {
enqueueSnackbar("Превышена длина вводимого текста")
} else {
setTitle(target.value || " ")
}
}}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
inputProps={{
maxLength: maxLengthTextField,
// maxLength: maxLengthTextField,
}}
InputProps={{
startAdornment: (

@ -1,9 +1,9 @@
import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useCallback, useState } from "react";
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
import { EnterIcon } from "@/assets/icons/questionsPage/enterIcon";
import { useAddAnswer } from "../../../utils/hooks/useAddAnswer";
import { AnswerDraggableList } from "../AnswerDraggableList";
import ButtonsOptions from "../QuestionOptions/ButtonsOptions";
import ButtonsOptions from "../QuestionOptions/ButtonsLayout/ButtonsOptions";
import SwitchDropDown from "./switchDropDown";
import type { QuizQuestionSelect } from "@frontend/squzanswerer";
@ -16,7 +16,7 @@ interface Props {
}
export default function DropDown({ question, openBranchingPage, setOpenBranchingPage }: Props) {
const onClickAddAnAnswer = useAddAnswer();
const {onClickAddAnAnswer} = useAddAnswer();
const [switchState, setSwitchState] = useState("setting");
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
@ -100,7 +100,7 @@ export default function DropDown({ question, openBranchingPage, setOpenBranching
</Box>
<ButtonsOptions
switchState={switchState}
SSHC={setSwitchState}
setSwitchState={setSwitchState}
questionId={question.id}
questionContentId={question.content.id}
questionType={question.type}

@ -2,11 +2,11 @@ import { Box, Link, Popover, Typography, useMediaQuery, useTheme } from "@mui/ma
import { updateQuestion } from "@root/questions/actions";
import { EmojiPicker } from "@ui_kit/EmojiPicker";
import { useState } from "react";
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
import { EnterIcon } from "@/assets/icons/questionsPage/enterIcon";
import type { QuizQuestionEmoji } from "@frontend/squzanswerer";
import { useAddAnswer } from "../../../utils/hooks/useAddAnswer";
import { AnswerDraggableList } from "../AnswerDraggableList";
import ButtonsOptions from "../QuestionOptions/ButtonsOptions";
import ButtonsOptions from "../QuestionOptions/ButtonsLayout/ButtonsOptions";
import EmojiAnswerItem from "./EmojiAnswerItem/EmojiAnswerItem";
import SwitchEmoji from "./switchEmoji";
@ -18,7 +18,7 @@ interface Props {
export default function Emoji({ question, openBranchingPage, setOpenBranchingPage }: Props) {
const [switchState, setSwitchState] = useState<string>("setting");
const onClickAddAnAnswer = useAddAnswer();
const {onClickAddAnAnswer} = useAddAnswer();
const [open, setOpen] = useState<boolean>(false);
const [anchorElement, setAnchorElement] = useState<HTMLDivElement | null>(null);
const [selectedVariant, setSelectedVariant] = useState<string | null>(null);
@ -29,9 +29,12 @@ export default function Emoji({ question, openBranchingPage, setOpenBranchingPag
return (
<>
<Box sx={{ padding: "20px" }}>
<AnswerDraggableList
questionId={question.id}
variants={question.content.variants.map((variant, index) => (
variants={question.content.variants
.filter(variant => !variant.isOwn ? true : question.content.own && variant.isOwn)
.map((variant, index) => (
<EmojiAnswerItem
key={variant.id}
index={index}
@ -42,9 +45,12 @@ export default function Emoji({ question, openBranchingPage, setOpenBranchingPag
setAnchorElement={setAnchorElement}
setOpen={setOpen}
setSelectedVariant={setSelectedVariant}
isOwn={Boolean(variant?.isOwn)}
ownPlaceholder={question.content.ownPlaceholder}
/>
))}
/>
<Popover
open={open}
anchorEl={anchorElement}
@ -115,7 +121,7 @@ export default function Emoji({ question, openBranchingPage, setOpenBranchingPag
</Box>
<ButtonsOptions
switchState={switchState}
SSHC={setSwitchState}
setSwitchState={setSwitchState}
questionId={question.id}
questionContentId={question.content.id}
questionType={question.type}

@ -2,6 +2,7 @@ import { ComponentPropsWithoutRef, useMemo } from "react";
import AnswerItem from "../../AnswerDraggableList/AnswerItem";
import VariantAdornment from "./VariantAdornment";
import VariantAdornmentMobile from "./VariantAdornmentMobile";
import { updateQuestion } from "@/stores/questions/actions";
type Props = Omit<
ComponentPropsWithoutRef<typeof AnswerItem>,
@ -11,6 +12,8 @@ type Props = Omit<
setAnchorElement: React.Dispatch<React.SetStateAction<HTMLDivElement | null>>;
setSelectedVariant: React.Dispatch<React.SetStateAction<string | null>>;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
isOwn: boolean;
ownPlaceholder: string;
};
export default function EmojiAnswerItem({
@ -23,7 +26,11 @@ export default function EmojiAnswerItem({
setAnchorElement,
setSelectedVariant,
setOpen,
isOwn,
ownPlaceholder,
}: Props) {
const addOrEditImageButton = useMemo(() => {
return (
!isTablet && (
@ -77,6 +84,8 @@ export default function EmojiAnswerItem({
variant={variant}
additionalContent={addOrEditImageButton}
additionalMobile={addOrEditImageButtonMobile}
isOwn={isOwn}
ownPlaceholder={ownPlaceholder}
/>
);
}

@ -1,23 +1,37 @@
import type { QuizQuestionEmoji, QuizQuestionVariant } from "@frontend/squzanswerer";
import type { QuestionType, QuizQuestionEmoji, QuizQuestionVariant } from "@frontend/squzanswerer";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
import { memo } from "react";
import CustomTextField from "@ui_kit/CustomTextField";
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
type SettingEmojiProps = {
question: QuizQuestionEmoji;
questionId: string;
isRequired: boolean;
isMulti: boolean;
isOwn: boolean;
isLargeCheck?: boolean;
};
const SettingEmoji = memo<SettingEmojiProps>(function ({ questionId, isRequired, isMulti, isOwn }) {
const SettingEmoji = memo<SettingEmojiProps>(function ({ question, questionId, isRequired, isLargeCheck, isMulti, isOwn }) {
const theme = useTheme();
const {switchOwn} = useAddAnswer();
const isWrappColumn = useMediaQuery(theme.breakpoints.down(980));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const isTablet = useMediaQuery(theme.breakpoints.down(985));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const setOwnPlaceholder = (replText: string) => {
updateQuestion(questionId, (question) => {
if (question.type !== "varimg") return;
question.content.ownPlaceholder = replText;
});
};
return (
<Box
sx={{
@ -29,7 +43,7 @@ const SettingEmoji = memo<SettingEmojiProps>(function ({ questionId, isRequired,
pt: isTablet ? "5px" : "0px",
}}
>
{/* <Box
<Box
sx={{
boxSizing: "border-box",
pt: "20px",
@ -50,6 +64,17 @@ const SettingEmoji = memo<SettingEmojiProps>(function ({ questionId, isRequired,
>
Настройки ответов
</Typography>
{/* <CustomCheckbox
dataCy="checkbox-long-text-answer"
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Многострочный ответ"}
checked={isLargeCheck}
handleChange={({ target }) => {
updateQuestion<QuizQuestionVariant>(questionId, (question) => {
question.content.largeCheck = target.checked;
});
}}
/> */}
<CustomCheckbox
dataCy="checkbox-multiple-answers"
sx={{ mr: isMobile ? "0px" : "16px" }}
@ -67,12 +92,34 @@ const SettingEmoji = memo<SettingEmojiProps>(function ({ questionId, isRequired,
label={'Вариант "свой ответ"'}
checked={isOwn}
handleChange={({ target }) => {
updateQuestion<QuizQuestionVariant>(questionId, (question) => {
question.content.own = target.checked;
});
switchOwn({question, checked:target.checked})
}}
/>
</Box> */}
{/* <Box sx={{ mt: isMobile ? "11px" : "6px", width: "100%" }}>
<Typography
sx={{
height: isMobile ? "18px" : "auto",
fontWeight: "500",
fontSize: "18px",
color: " #4D4D4D",
mb: "14px",
}}
>
Подсказка "своего ответа"
</Typography>
<CustomTextField
sx={{
maxWidth: "330px",
width: "100%",
mr: isMobile ? "0px" : "16px",
}}
maxLength={60}
placeholder={"мой ответ: три"}
value={ownPlaceholder}
onChange={({ target }) => setOwnPlaceholder(target.value)}
/>
</Box> */}
</Box>
<Box
sx={{
pt: "20px",

@ -12,10 +12,13 @@ export default function SwitchEmoji({ switchState = "setting", question }: Props
case "setting":
return (
<SettingEmoji
question={question}
questionId={question.id}
isRequired={question.content.required}
isOwn={question.content.own}
isMulti={question.content.multi}
isLargeCheck={question.content.isLargeCheck}
ownPlaceholder={question.content.ownPlaceholder}
/>
);
case "help":

@ -5,8 +5,8 @@ import {
} from "@root/quizes/actions";
import QuizPreview from "@ui_kit/QuizPreview/QuizPreview";
import { createPortal } from "react-dom";
import AddAnswer from "../../../assets/icons/questionsPage/addAnswer";
import ArrowLeft from "../../../assets/icons/questionsPage/arrowLeft";
import AddAnswer from "@/assets/icons/questionsPage/addAnswer";
import ArrowLeft from "@/assets/icons/questionsPage/arrowLeft";
import { FormDraggableList } from "./FormDraggableList";
import {
collapseAllQuestions,

@ -3,12 +3,12 @@ import { Box } from "@mui/material";
import QuestionsMiniButton from "@ui_kit/QuestionsMiniButton";
import { BUTTON_TYPE_QUESTIONS } from "../TypeQuestions";
import Answer from "../../../assets/icons/questionsPage/answer";
import Date from "../../../assets/icons/questionsPage/date";
import Download from "../../../assets/icons/questionsPage/download";
import DropDown from "../../../assets/icons/questionsPage/drop_down";
import Input from "../../../assets/icons/questionsPage/input";
import Slider from "../../../assets/icons/questionsPage/slider";
import Answer from "@/assets/icons/questionsPage/answer";
import Date from "@/assets/icons/questionsPage/date";
import Download from "@/assets/icons/questionsPage/download";
import DropDown from "@/assets/icons/questionsPage/drop_down";
import Input from "@/assets/icons/questionsPage/input";
import Slider from "@/assets/icons/questionsPage/slider";
import { QuestionType } from "@model/question/question";
import { createTypedQuestion } from "@root/questions/actions";

@ -3,9 +3,9 @@ import { updateQuestion } from "@root/questions/actions";
import CustomTextField from "@ui_kit/CustomTextField";
import TooltipClickInfo from "@ui_kit/Toolbars/TooltipClickInfo";
import { useEffect, useState } from "react";
import InfoIcon from "../../../assets/icons/InfoIcon";
import InfoIcon from "@/assets/icons/InfoIcon";
import type { QuizQuestionText } from "@frontend/squzanswerer";
import ButtonsOptionsAndPict from "../QuestionOptions/ButtonsOptionsAndPict";
import ButtonsOptions from "../QuestionOptions/ButtonsLayout/ButtonsOptions";
import SwitchTextField from "./switchTextField";
interface Props {
@ -90,9 +90,9 @@ export default function OwnTextField({ question, openBranchingPage, setOpenBranc
)}
</Box>
</Box>
<ButtonsOptionsAndPict
<ButtonsOptions
switchState={switchState}
SSHC={setSwitchState}
setSwitchState={setSwitchState}
questionId={question.id}
questionContentId={question.content.id}
questionHasParent={question.content.rule.parentId?.length !== 0}

@ -0,0 +1,240 @@
import { QuestionType } from "@model/question/question";
import {
Box,
Button,
IconButton,
Modal,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import {
copyQuestion,
deleteQuestion,
deleteQuestionWithTimeout,
} from "@root/questions/actions";
import { useQuestionsStore } from "@root/questions/store";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { updateDesireToOpenABranchingModal } from "@root/uiTools/actions";
import MiniButtonSetting from "@ui_kit/MiniButtonSetting";
import { ReallyChangingModal } from "@ui_kit/Modal/ReallyChangingModal/ReallyChangingModal";
import { DeleteFunction } from "@utils/deleteFunc";
import { memo, useState } from "react";
import { CopyIcon } from "@/assets/icons/questionsPage/CopyIcon";
import Branching from "@/assets/icons/questionsPage/branching";
import { DeleteIcon } from "@/assets/icons/questionsPage/deleteIcon";
import ImgIcon from "@/assets/icons/questionsPage/imgIcon";
import SettingIcon from "@/assets/icons/questionsPage/settingIcon";
import { DeleteBranchingQuestionModal } from "./DeleteBranchingQuestionModal";
interface Props {
switchState: string;
setSwitchState: (data: string) => void;
openBranchingPage: boolean;
setOpenBranchingPage: (a: boolean) => void;
questionId: string;
questionType: QuestionType;
questionContentId: string;
questionHasParent: boolean;
};
const IgnoreImage = ["images", "emoji", "number", "date", "select", "file", "rating"]
const ButtonsOptions = memo<Props>(function ({
setSwitchState,
switchState,
openBranchingPage,
setOpenBranchingPage,
questionId,
questionType,
questionContentId,
questionHasParent,
}) {
const theme = useTheme();
const quiz = useCurrentQuiz();
const isQuestionFirst = useQuestionsStore((state) => state.questions[0]?.id === questionId,);
const isIconMobile = useMediaQuery(theme.breakpoints.down(1050));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isIgnoreImage = !IgnoreImage.includes(questionType)
const [buttonHover, setButtonHover] = useState<string>("");
const [openedReallyChangingModal, setOpenedReallyChangingModal] = useState<boolean>(false);
const [openDelete, setOpenDelete] = useState<boolean>(false);
if (!quiz) return null;
return (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
background: "#f2f3f7",
height: isMobile ? "92px" : "70px",
}}
>
<Box
sx={{
padding: isMobile ? " 3px 12px 11px" : "20px",
display: "flex",
flexWrap: isMobile ? "wrap" : "nowrap",
gap: "6px",
maxWidth: isMobile ? "200px" : undefined,
}}
>
<MiniButtonSetting
onMouseEnter={() => setButtonHover("setting")}
onMouseLeave={() => setButtonHover("")}
onClick={() => {
switchState === "setting" ? setSwitchState("") : setSwitchState("setting");
}}
sx={{
maxWidth: "104px",
minWidth: isIconMobile ? "30px" : "64px",
height: "30px",
backgroundColor:
switchState === "setting"
? theme.palette.brightPurple.main
: "transparent",
color:
switchState === "setting" ? "#ffffff" : theme.palette.grey3.main,
"&:hover": {
color:
switchState === "setting" ? theme.palette.grey3.main : null,
},
}}
>
<SettingIcon
color={
buttonHover === "setting"
? theme.palette.grey3.main
: switchState === "setting"
? "#ffffff"
: theme.palette.grey3.main
}
/>
{isIconMobile ? null : "Настройки"}
</MiniButtonSetting>
{questionType !== "text" && (
<MiniButtonSetting
onMouseEnter={() => setButtonHover("branching")}
onMouseLeave={() => setButtonHover("")}
onClick={() => {
setOpenBranchingPage(true);
updateDesireToOpenABranchingModal(questionContentId);
}}
sx={{
display: quiz.config.type === "form" ? "none" : "flex",
height: "30px",
maxWidth: "103px",
minWidth: isIconMobile ? "30px" : "64px",
backgroundColor:
switchState === "branching"
? theme.palette.brightPurple.main
: "transparent",
color:
switchState === "branching"
? "#ffffff"
: theme.palette.grey3.main,
"&:hover": {
color:
switchState === "branching" ? theme.palette.grey3.main : null,
},
}}
>
<Branching
color={
buttonHover === "branching"
? theme.palette.grey3.main
: switchState === "branching"
? "#ffffff"
: theme.palette.grey3.main
}
/>
{isIconMobile ? null : "Ветвление"}
</MiniButtonSetting>
)}
{isIgnoreImage &&
<MiniButtonSetting
onMouseEnter={() => setButtonHover("image")}
onMouseLeave={() => setButtonHover("")}
onClick={() => {
switchState === "image" ? setSwitchState("") : setSwitchState("image");
}}
sx={{
height: "30px",
maxWidth: "123px",
minWidth: isIconMobile ? "30px" : "64px",
backgroundColor:
switchState === "image"
? theme.palette.brightPurple.main
: "transparent",
color:
switchState === "image" ? "#ffffff" : theme.palette.grey3.main,
"&:hover": {
color: switchState === "image" ? theme.palette.grey3.main : null,
},
}}
>
<ImgIcon
color={
buttonHover === "image"
? theme.palette.grey3.main
: switchState === "image"
? "#ffffff"
: theme.palette.grey3.main
}
/>
{isIconMobile ? null : "Изображение"}
</MiniButtonSetting>
}
</Box>
<Box
sx={{
padding: "20px",
}}
>
<IconButton
sx={{ borderRadius: "6px" }}
onClick={() => copyQuestion(questionId, quiz.backendId)}
>
<CopyIcon style={{ color: "#4D4D4D" }} />
</IconButton>
{(quiz?.config.type !== "form" || !isQuestionFirst) && (
<IconButton
sx={{ borderRadius: "6px", padding: "2px" }}
onClick={() => {
if (questionType === null) {
deleteQuestion(questionId);
}
if (questionHasParent) {
setOpenDelete(true);
} else {
deleteQuestionWithTimeout(questionId, () =>
DeleteFunction(questionId),
);
}
}}
data-cy="delete-question"
>
<DeleteIcon style={{ color: "#4D4D4D" }} />
</IconButton>
)}
</Box>
<DeleteBranchingQuestionModal
open={openDelete}
onclose={() => setOpenDelete(false)}
questionId={questionId}
/>
<ReallyChangingModal
opened={openedReallyChangingModal}
onClose={() => setOpenedReallyChangingModal(false)}
/>
</Box>
);
});
export default ButtonsOptions;

@ -0,0 +1,66 @@
import { deleteQuestionWithTimeout } from "@/stores/questions/actions";
import { DeleteFunction } from "@/utils/deleteFunc";
import { Box, Button, Modal, Typography } from "@mui/material";
interface Props {
open: boolean;
onclose: () => void;
questionId: string;
}
export const DeleteBranchingQuestionModal = ({
open,
onclose,
questionId,
}: Props) => {
return (
<Modal
open={open}
onClose={onclose}
>
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
padding: "30px",
borderRadius: "10px",
background: "#FFFFFF",
}}
>
<Typography
variant="h6"
sx={{ textAlign: "center" }}
>
Вы удаляете вопрос, участвующий в ветвлении. Все его потомки потеряют данные ветвления. Вы уверены, что хотите удалить вопрос?
</Typography>
<Box
sx={{
marginTop: "30px",
display: "flex",
justifyContent: "center",
gap: "15px",
}}
>
<Button
variant="contained"
sx={{ minWidth: "150px" }}
onClick={onclose}
>
Отмена
</Button>
<Button
variant="contained"
sx={{ minWidth: "150px" }}
onClick={() => {
deleteQuestionWithTimeout(questionId, () => DeleteFunction(questionId));
}}
>
Подтвердить
</Button>
</Box>
</Box>
</Modal>
)
}

@ -0,0 +1,184 @@
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import type { SxProps } from "@mui/material";
import { Box, Button, IconButton, Modal, Typography, useMediaQuery, useTheme } from "@mui/material";
import { copyQuestion, deleteQuestion, deleteQuestionWithTimeout } from "@root/questions/actions";
import { useQuestionsStore } from "@root/questions/store";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { updateDesireToOpenABranchingModal } from "@root/uiTools/actions";
import MiniButtonSetting from "@ui_kit/MiniButtonSetting";
import { DeleteFunction } from "@utils/deleteFunc";
import { memo, useState } from "react";
import { CopyIcon } from "@/assets/icons/questionsPage/CopyIcon";
import Branching from "@/assets/icons/questionsPage/branching";
import SettingIcon from "@/assets/icons/questionsPage/settingIcon";
import { QuestionType } from "@model/question/question";
import type { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
import { DeleteBranchingQuestionModal } from "./DeleteBranchingQuestionModal";
interface Props {
switchState: string;
setSwitchState: (data: string) => void;
questionId: string;
questionContentId: string;
questionType: QuestionType;
questionHasParent: boolean;
setOpenBranchingPage: (a: boolean) => void;
sx?: SxProps;
}
export default memo<Props>(function ({
setSwitchState,
switchState,
questionId,
questionContentId,
questionType,
questionHasParent,
setOpenBranchingPage,
}) {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isWrappMiniButtonSetting = useMediaQuery(theme.breakpoints.down(920));
const quiz = useCurrentQuiz();
const [openDelete, setOpenDelete] = useState<boolean>(false);
const isQuestionFirst = useQuestionsStore((state) => state.questions[0]?.id === questionId);
if (!quiz) return null;
const openedModal = () => {
setOpenBranchingPage(true);
updateDesireToOpenABranchingModal(questionContentId);
};
const buttonSetting: {
icon: JSX.Element;
title: string;
value: string;
myFunc?: any;
}[] = [
{
icon: <SettingIcon color={switchState === "setting" ? "#ffffff" : theme.palette.grey3.main} />,
title: "Настройки",
value: "setting",
},
{
icon: <Branching color={switchState === "branching" ? "#ffffff" : theme.palette.grey3.main} />,
title: "Ветвление",
value: "branching",
myFunc: (question: AnyTypedQuizQuestion) => {
setOpenBranchingPage(true);
updateDesireToOpenABranchingModal(question.content.id);
},
},
];
return (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
background: "#f2f3f7",
height: isMobile ? "92px" : "70px",
}}
>
ButtonsOptions
<Box
sx={{
padding: isMobile ? " 3px 12px 11px" : "20px",
display: "flex",
flexWrap: isMobile ? "wrap" : "nowrap",
gap: "6px",
}}
>
{buttonSetting.map(({ icon, title, value, myFunc }) => (
<Box key={value}>
{value === "branching" ? (
["page", "text", "date", "number"].includes(questionType) ? null : (
<MiniButtonSetting
key={title}
onClick={() => {
openedModal();
}}
sx={{
display: quiz.config.type === "form" ? "none" : "flex",
backgroundColor: switchState === value ? theme.palette.brightPurple.main : "transparent",
color: switchState === value ? "#ffffff" : theme.palette.grey3.main,
minWidth: isWrappMiniButtonSetting ? "30px" : "64px",
height: "30px",
"&:hover": {
color: theme.palette.grey3.main,
"& path": { stroke: theme.palette.grey3.main },
},
}}
>
{icon}
{isWrappMiniButtonSetting ? null : title}
</MiniButtonSetting>
)
) : (
<MiniButtonSetting
key={title}
onClick={() => {
setSwitchState(value);
myFunc();
}}
sx={{
backgroundColor: switchState === value ? theme.palette.brightPurple.main : "transparent",
color: switchState === value ? "#ffffff" : theme.palette.grey3.main,
minWidth: isWrappMiniButtonSetting ? "30px" : "64px",
height: "30px",
"&:hover": {
color: theme.palette.grey3.main,
"& path": { stroke: theme.palette.grey3.main },
},
}}
>
{icon}
{isWrappMiniButtonSetting ? null : title}
</MiniButtonSetting>
)}
</Box>
))}
</Box>
<Box
sx={{
padding: "20px",
display: "flex",
gap: "6px",
}}
>
<IconButton
sx={{ borderRadius: "6px", padding: "2px" }}
onClick={() => copyQuestion(questionId, quiz.backendId)}
>
<CopyIcon color={"#4D4D4D"} />
</IconButton>
{(quiz?.config.type !== "form" || !isQuestionFirst) && (
<IconButton
sx={{ borderRadius: "6px", padding: "2px" }}
onClick={() => {
if (questionType === null) {
deleteQuestion(questionId);
}
if (questionHasParent) {
setOpenDelete(true);
} else {
deleteQuestionWithTimeout(questionId, () => DeleteFunction(questionId));
}
}}
data-cy="delete-question"
>
<DeleteIcon color={"#4D4D4D"} />
</IconButton>
)}
<DeleteBranchingQuestionModal
open={openDelete}
onclose={() => setOpenDelete(false)}
questionId={questionId}
/>
</Box>
</Box>
);
});

@ -8,15 +8,16 @@ import { updateDesireToOpenABranchingModal } from "@root/uiTools/actions";
import MiniButtonSetting from "@ui_kit/MiniButtonSetting";
import { DeleteFunction } from "@utils/deleteFunc";
import { memo, useState } from "react";
import { CopyIcon } from "../../../assets/icons/questionsPage/CopyIcon";
import Branching from "../../../assets/icons/questionsPage/branching";
import SettingIcon from "../../../assets/icons/questionsPage/settingIcon";
import { CopyIcon } from "@/assets/icons/questionsPage/CopyIcon";
import Branching from "@/assets/icons/questionsPage/branching";
import SettingIcon from "@/assets/icons/questionsPage/settingIcon";
import { QuestionType } from "@model/question/question";
import type { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
import { DeleteBranchingQuestionModal } from "./ButtonsLayout/DeleteBranchingQuestionModal";
interface Props {
switchState: string;
SSHC: (data: string) => void;
setSwitchState: (data: string) => void;
questionId: string;
questionContentId: string;
questionType: QuestionType;
@ -25,8 +26,8 @@ interface Props {
sx?: SxProps;
}
const ButtonsOptions = memo<Props>(function ({
SSHC,
export default memo<Props>(function ({
setSwitchState,
switchState,
questionId,
questionContentId,
@ -54,21 +55,21 @@ const ButtonsOptions = memo<Props>(function ({
value: string;
myFunc?: any;
}[] = [
{
icon: <SettingIcon color={switchState === "setting" ? "#ffffff" : theme.palette.grey3.main} />,
title: "Настройки",
value: "setting",
},
{
icon: <Branching color={switchState === "branching" ? "#ffffff" : theme.palette.grey3.main} />,
title: "Ветвление",
value: "branching",
myFunc: (question: AnyTypedQuizQuestion) => {
setOpenBranchingPage(true);
updateDesireToOpenABranchingModal(question.content.id);
{
icon: <SettingIcon color={switchState === "setting" ? "#ffffff" : theme.palette.grey3.main} />,
title: "Настройки",
value: "setting",
},
},
];
{
icon: <Branching color={switchState === "branching" ? "#ffffff" : theme.palette.grey3.main} />,
title: "Ветвление",
value: "branching",
myFunc: (question: AnyTypedQuizQuestion) => {
setOpenBranchingPage(true);
updateDesireToOpenABranchingModal(question.content.id);
},
},
];
return (
<Box
@ -81,6 +82,7 @@ const ButtonsOptions = memo<Props>(function ({
height: isMobile ? "92px" : "70px",
}}
>
ButtonsOptions
<Box
sx={{
padding: isMobile ? " 3px 12px 11px" : "20px",
@ -118,7 +120,7 @@ const ButtonsOptions = memo<Props>(function ({
<MiniButtonSetting
key={title}
onClick={() => {
SSHC(value);
setSwitchState(value);
myFunc();
}}
sx={{
@ -170,60 +172,12 @@ const ButtonsOptions = memo<Props>(function ({
<DeleteIcon color={"#4D4D4D"} />
</IconButton>
)}
<Modal
<DeleteBranchingQuestionModal
open={openDelete}
onClose={() => setOpenDelete(false)}
>
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
padding: "30px",
borderRadius: "10px",
background: "#FFFFFF",
}}
>
<Typography
variant="h6"
sx={{ textAlign: "center" }}
>
Вы удаляете вопрос, участвующий в ветвлении. Все его потомки потеряют данные ветвления. Вы уверены, что
хотите удалить вопрос?
</Typography>
<Box
sx={{
marginTop: "30px",
display: "flex",
justifyContent: "center",
gap: "15px",
}}
>
<Button
variant="contained"
sx={{ minWidth: "150px" }}
onClick={() => setOpenDelete(false)}
>
Отмена
</Button>
<Button
variant="contained"
sx={{ minWidth: "150px" }}
onClick={() => {
deleteQuestionWithTimeout(questionId, () => DeleteFunction(questionId));
}}
>
Подтвердить
</Button>
</Box>
</Box>
</Modal>
onclose={() => setOpenDelete(false)}
questionId={questionId}
/>
</Box>
</Box>
);
});
ButtonsOptions.displayName = "ButtonsOptions";
export default ButtonsOptions;
});

@ -1,3 +1,4 @@
import { QuestionType } from "@model/question/question";
import {
Box,
@ -20,25 +21,28 @@ import MiniButtonSetting from "@ui_kit/MiniButtonSetting";
import { ReallyChangingModal } from "@ui_kit/Modal/ReallyChangingModal/ReallyChangingModal";
import { DeleteFunction } from "@utils/deleteFunc";
import { memo, useState } from "react";
import { CopyIcon } from "../../../assets/icons/questionsPage/CopyIcon";
import Branching from "../../../assets/icons/questionsPage/branching";
import { DeleteIcon } from "../../../assets/icons/questionsPage/deleteIcon";
import ImgIcon from "../../../assets/icons/questionsPage/imgIcon";
import SettingIcon from "../../../assets/icons/questionsPage/settingIcon";
import { CopyIcon } from "@/assets/icons/questionsPage/CopyIcon";
import Branching from "@/assets/icons/questionsPage/branching";
import { DeleteIcon } from "@/assets/icons/questionsPage/deleteIcon";
import ImgIcon from "@/assets/icons/questionsPage/imgIcon";
import SettingIcon from "@/assets/icons/questionsPage/settingIcon";
import { DeleteBranchingQuestionModal } from "./ButtonsLayout/DeleteBranchingQuestionModal";
interface Props {
switchState: string;
SSHC: (data: string) => void;
setSwitchState: (data: string) => void;
openBranchingPage: boolean;
setOpenBranchingPage: (a: boolean) => void;
questionId: string;
questionType: QuestionType;
questionContentId: string;
questionHasParent: boolean;
}
};
const ButtonsOptionsAndPict = memo<Props>(function ({
SSHC,
const IgnoreImage = ["images", "emoji", "number", "date", "select", "file", "rating"]
const ButtonsOptions = memo<Props>(function ({
setSwitchState,
switchState,
openBranchingPage,
setOpenBranchingPage,
@ -47,17 +51,17 @@ const ButtonsOptionsAndPict = memo<Props>(function ({
questionContentId,
questionHasParent,
}) {
const [buttonHover, setButtonHover] = useState<string>("");
const [openedReallyChangingModal, setOpenedReallyChangingModal] =
useState<boolean>(false);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isIconMobile = useMediaQuery(theme.breakpoints.down(1050));
const quiz = useCurrentQuiz();
const isQuestionFirst = useQuestionsStore((state) => state.questions[0]?.id === questionId,);
const isIconMobile = useMediaQuery(theme.breakpoints.down(1050));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isIgnoreImage = !IgnoreImage.includes(questionType)
const [buttonHover, setButtonHover] = useState<string>("");
const [openedReallyChangingModal, setOpenedReallyChangingModal] = useState<boolean>(false);
const [openDelete, setOpenDelete] = useState<boolean>(false);
const isQuestionFirst = useQuestionsStore(
(state) => state.questions[0]?.id === questionId,
);
if (!quiz) return null;
@ -85,7 +89,7 @@ const ButtonsOptionsAndPict = memo<Props>(function ({
onMouseEnter={() => setButtonHover("setting")}
onMouseLeave={() => setButtonHover("")}
onClick={() => {
SSHC("setting");
switchState === "setting" ? setSwitchState("") : setSwitchState("setting");
}}
sx={{
maxWidth: "104px",
@ -153,38 +157,40 @@ const ButtonsOptionsAndPict = memo<Props>(function ({
{isIconMobile ? null : "Ветвление"}
</MiniButtonSetting>
)}
<MiniButtonSetting
onMouseEnter={() => setButtonHover("image")}
onMouseLeave={() => setButtonHover("")}
onClick={() => {
SSHC("image");
}}
sx={{
height: "30px",
maxWidth: "123px",
minWidth: isIconMobile ? "30px" : "64px",
backgroundColor:
switchState === "image"
? theme.palette.brightPurple.main
: "transparent",
color:
switchState === "image" ? "#ffffff" : theme.palette.grey3.main,
"&:hover": {
color: switchState === "image" ? theme.palette.grey3.main : null,
},
}}
>
<ImgIcon
color={
buttonHover === "image"
? theme.palette.grey3.main
: switchState === "image"
? "#ffffff"
: theme.palette.grey3.main
}
/>
{isIconMobile ? null : "Изображение"}
</MiniButtonSetting>
{isIgnoreImage &&
<MiniButtonSetting
onMouseEnter={() => setButtonHover("image")}
onMouseLeave={() => setButtonHover("")}
onClick={() => {
switchState === "image" ? setSwitchState("") : setSwitchState("image");
}}
sx={{
height: "30px",
maxWidth: "123px",
minWidth: isIconMobile ? "30px" : "64px",
backgroundColor:
switchState === "image"
? theme.palette.brightPurple.main
: "transparent",
color:
switchState === "image" ? "#ffffff" : theme.palette.grey3.main,
"&:hover": {
color: switchState === "image" ? theme.palette.grey3.main : null,
},
}}
>
<ImgIcon
color={
buttonHover === "image"
? theme.palette.grey3.main
: switchState === "image"
? "#ffffff"
: theme.palette.grey3.main
}
/>
{isIconMobile ? null : "Изображение"}
</MiniButtonSetting>
}
</Box>
<Box
sx={{
@ -217,52 +223,12 @@ const ButtonsOptionsAndPict = memo<Props>(function ({
<DeleteIcon style={{ color: "#4D4D4D" }} />
</IconButton>
)}
<Modal open={openDelete} onClose={() => setOpenDelete(false)}>
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
padding: "30px",
borderRadius: "10px",
background: "#FFFFFF",
}}
>
<Typography variant="h6" sx={{ textAlign: "center" }}>
Вы удаляете вопрос, участвующий в ветвлении. Все его потомки
потеряют данные ветвления. Вы уверены, что хотите удалить вопрос?
</Typography>
<Box
sx={{
marginTop: "30px",
display: "flex",
justifyContent: "center",
gap: "15px",
}}
>
<Button
variant="contained"
sx={{ minWidth: "150px" }}
onClick={() => setOpenDelete(false)}
>
Отмена
</Button>
<Button
variant="contained"
sx={{ minWidth: "150px" }}
onClick={() => {
deleteQuestionWithTimeout(questionId, () =>
DeleteFunction(questionId),
);
}}
>
Подтвердить
</Button>
</Box>
</Box>
</Modal>
</Box>
<DeleteBranchingQuestionModal
open={openDelete}
onclose={() => setOpenDelete(false)}
questionId={questionId}
/>
<ReallyChangingModal
opened={openedReallyChangingModal}
onClose={() => setOpenedReallyChangingModal(false)}
@ -271,6 +237,4 @@ const ButtonsOptionsAndPict = memo<Props>(function ({
);
});
ButtonsOptionsAndPict.displayName = "ButtonsOptionsAndPict";
export default ButtonsOptionsAndPict;
export default ButtonsOptions;

@ -12,7 +12,7 @@ interface Props {
setOpenBranchingPage: (a: boolean) => void;
}
export default function DataOptions({ question, openBranchingPage, setOpenBranchingPage }: Props) {
export default function DateOptions({ question, openBranchingPage, setOpenBranchingPage }: Props) {
const [switchState, setSwitchState] = useState("setting");
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
@ -64,7 +64,7 @@ export default function DataOptions({ question, openBranchingPage, setOpenBranch
</Box>
<ButtonsOptions
switchState={switchState}
SSHC={setSwitchState}
setSwitchState={setSwitchState}
questionId={question.id}
questionContentId={question.content.id}
questionType={question.type}

@ -6,11 +6,11 @@ import type { QuizQuestionDate } from "@frontend/squzanswerer";
type SettingsDataProps = {
questionId: string;
isRequired: boolean;
isDateRange: boolean;
isRange: boolean;
isTime: boolean;
};
export default function SettingsData({ questionId, isRequired, isDateRange, isTime }: SettingsDataProps) {
export default function SettingsData({ questionId, isRequired, isRange, isTime }: SettingsDataProps) {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isTablet = useMediaQuery(theme.breakpoints.down(900));
@ -28,7 +28,7 @@ export default function SettingsData({ questionId, isRequired, isDateRange, isTi
pt: isTablet ? "5px" : "0px",
}}
>
{/* <Box
<Box
sx={{
boxSizing: "border-box",
pt: "20px",
@ -53,14 +53,14 @@ export default function SettingsData({ questionId, isRequired, isDateRange, isTi
dataCy="checkbox-dateRange"
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Выбор диапазона дат"}
checked={isDateRange}
checked={isRange}
handleChange={({ target }) => {
updateQuestion<QuizQuestionDate>(questionId, (question) => {
question.content.dateRange = target.checked;
question.content.isRange = target.checked;
});
}}
/>
<CustomCheckbox
{/* <CustomCheckbox
dataCy="checkbox-time"
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Выбор времени"}
@ -70,8 +70,8 @@ export default function SettingsData({ questionId, isRequired, isDateRange, isTi
question.content.time = target.checked;
});
}}
/>
</Box> */}
/> */}
</Box>
<Box
sx={{
boxSizing: "border-box",

@ -14,7 +14,7 @@ export default function SwitchData({ switchState = "setting", question }: Props)
<SettingData
questionId={question.id}
isRequired={question.content.required}
isDateRange={question.content.dateRange}
isRange={question.content.isRange}
isTime={question.content.time}
/>
);

@ -0,0 +1,80 @@
import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useState } from "react";
import InfoIcon from "@/assets/icons/InfoIcon";
import ButtonsOptions from "../ButtonsLayout/ButtonsOptions";
import SwitchDate from "./switchDate";
import TooltipClickInfo from "@ui_kit/Toolbars/TooltipClickInfo";
import { QuizQuestionDate } from "@frontend/squzanswerer";
interface Props {
question: QuizQuestionDate;
openBranchingPage: boolean;
setOpenBranchingPage: (a: boolean) => void;
}
export default function DateOptions({ question, openBranchingPage, setOpenBranchingPage }: Props) {
const [switchState, setSwitchState] = useState("setting");
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
return (
<>
<Box
sx={{
width: isMobile ? "auto" : "100%",
maxWidth: "493px",
display: "flex",
pl: "20px",
pr: "20px",
flexDirection: "column",
gap: isMobile ? "18px" : "20px",
}}
>
<Box
sx={{
display: "flex",
alignItems: "flex-start",
gap: "12px",
mb: "20px",
}}
>
<Typography
sx={{
fontWeight: 400,
fontSize: "16px",
lineHeight: "18.96px",
color: theme.palette.grey2.main,
}}
>
Пользователю будет предложено выбрать дату
</Typography>
{isMobile ? (
<TooltipClickInfo title={"Выбор даты."} />
) : (
<Tooltip
title="Выбор даты."
placement="top"
>
<Box>
<InfoIcon />
</Box>
</Tooltip>
)}
</Box>
</Box>
<ButtonsOptions
switchState={switchState}
setSwitchState={setSwitchState}
questionId={question.id}
questionContentId={question.content.id}
questionType={question.type}
questionHasParent={question.content.rule.parentId?.length !== 0}
setOpenBranchingPage={setOpenBranchingPage}
/>
<SwitchDate
switchState={switchState}
question={question}
/>
</>
);
}

@ -0,0 +1,110 @@
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
import type { QuizQuestionDate } from "@frontend/squzanswerer";
type SettingsDataProps = {
questionId: string;
isRequired: boolean;
isRange: boolean;
isTime: boolean;
};
export default function SettingsData({ questionId, isRequired, isRange, isTime }: SettingsDataProps) {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isTablet = useMediaQuery(theme.breakpoints.down(900));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
return (
<Box
sx={{
display: "flex",
justifyContent: "space-between",
flexDirection: isTablet ? "column" : "row",
marginRight: isFigmaTablte ? (isMobile ? "0" : "0px") : "30px",
pb: "20px",
pl: "20px",
pt: isTablet ? "5px" : "0px",
}}
>
<Box
sx={{
boxSizing: "border-box",
pt: "20px",
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
dataCy="checkbox-dateRange"
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Выбор диапазона дат"}
checked={isRange}
handleChange={({ target }) => {
updateQuestion<QuizQuestionDate>(questionId, (question) => {
question.content.isRange = target.checked;
});
}}
/>
{/* <CustomCheckbox
dataCy="checkbox-time"
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Выбор времени"}
checked={isTime}
handleChange={({ target }) => {
updateQuestion<QuizQuestionDate>(questionId, (question) => {
question.content.time = target.checked;
});
}}
/> */}
</Box>
<Box
sx={{
boxSizing: "border-box",
pt: "20px",
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
dataCy="checkbox-optional-question"
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Необязательный вопрос"}
checked={!isRequired}
handleChange={({ target }) => {
updateQuestion<QuizQuestionDate>(questionId, (question) => {
question.content.required = !target.checked;
});
}}
/>
</Box>
</Box>
);
}

@ -0,0 +1,32 @@
import { QuizQuestionDate } from "@frontend/squzanswerer";
import HelpQuestions from "../../helpQuestions";
import SettingDate from "./settingDate";
interface Props {
switchState: string;
question: QuizQuestionDate;
}
export default function SwitchData({ switchState = "setting", question }: Props) {
switch (switchState) {
case "setting":
return (
<SettingDate
questionId={question.id}
isRequired={question.content.required}
isRange={question.content.isRange}
isTime={question.content.time}
/>
);
case "help":
return (
<HelpQuestions
questionId={question.id}
hintText={question.content.hint.text}
hintVideo={question.content.hint.video}
/>
);
default:
return <></>;
}
}

@ -1,16 +1,22 @@
import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
import { addQuestionVariant, clearQuestionImages, uploadQuestionImage } from "@root/questions/actions";
import {
addQuestionVariant,
clearQuestionImages,
uploadQuestionImage,
} from "@root/questions/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { EnterIcon } from "@/assets/icons/questionsPage/enterIcon";
import type { QuizQuestionVarImg } from "@frontend/squzanswerer";
import type { QuizQuestionVarImg } from "@frontend/squzanswerer/dist-package/model/questionTypes/varimg";
import { useDisclosure } from "@/utils/useDisclosure";
import { AnswerDraggableList } from "../../AnswerDraggableList";
import ImageEditAnswerItem from "../../AnswerDraggableList/ImageEditAnswerItem";
import ButtonsOptionsAndPict from "../ButtonsOptionsAndPict";
import ButtonsOptions from "../ButtonsLayout/ButtonsOptions";
import { UploadImageModal } from "../../UploadImage/UploadImageModal";
import SwitchOptionsAndPict from "./switchOptionsAndPict";
import { CropModalInit } from "@/ui_kit/Modal/CropModal";
import imge from "@/assets/card-1.png"
interface Props {
question: QuizQuestionVarImg;
@ -18,33 +24,48 @@ interface Props {
setOpenBranchingPage: (a: boolean) => void;
}
export default function OptionsAndPicture({ question, setOpenBranchingPage }: Props) {
export default function OptionsAndPicture({
question,
setOpenBranchingPage,
}: Props) {
const [switchState, setSwitchState] = useState("setting");
const [pictureUploding, setPictureUploading] = useState<boolean>(false);
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
const [openCropModal, setOpenCropModal] = useState(false);
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(
null,
);
const variant = question.content.variants.find(
(variant) => variant.id === selectedVariantId,
)
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const quizQid = useCurrentQuiz()?.qid;
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
const { isCropModalOpen, openCropModal, closeCropModal, imageBlob, originalImageUrl, setCropModalImageBlob } =
useCropModalState();
const handleImageUpload = async (file: File) => {
if (!selectedVariantId) return;
setPictureUploading(true);
const url = await uploadQuestionImage(question.id, quizQid, file, (question, url) => {
if (!("variants" in question.content)) return;
const variant = question.content.variants.find((variant) => variant.id === selectedVariantId);
if (!variant) return;
variant.extendedText = url;
variant.originalImageUrl = url;
});
closeImageUploadModal();
openCropModal(file, url);
const url = await uploadQuestionImage(
question.id,
quizQid,
file,
(question, url) => {
if (!("variants" in question.content)) return;
const variant = question.content.variants.find(
(variant) => variant.id === selectedVariantId,
);
if (!variant) return;
variant.extendedText = url;
variant.originalImageUrl = url;
},
);
setOpenCropModal(true)
setPictureUploading(false);
};
@ -55,7 +76,9 @@ export default function OptionsAndPicture({ question, setOpenBranchingPage }: Pr
uploadQuestionImage(question.id, quizQid, imageBlob, (question, url) => {
if (!("variants" in question.content)) return;
const variant = question.content.variants.find((variant) => variant.id === selectedVariantId);
const variant = question.content.variants.find(
(variant) => variant.id === selectedVariantId,
);
if (!variant) return;
variant.extendedText = url;
@ -82,10 +105,12 @@ export default function OptionsAndPicture({ question, setOpenBranchingPage }: Pr
largeCheck={question.content.largeCheck}
variant={variant}
isMobile={isMobile}
openCropModal={openCropModal}
openCropModal={() => {setOpenCropModal(true)}}
openImageUploadModal={openImageUploadModal}
pictureUploding={pictureUploding}
setSelectedVariantId={setSelectedVariantId}
isOwn={Boolean(variant?.isOwn)}
ownPlaceholder={question.content.ownPlaceholder}
/>
))}
/>
@ -94,17 +119,16 @@ export default function OptionsAndPicture({ question, setOpenBranchingPage }: Pr
onClose={closeImageUploadModal}
handleImageChange={handleImageUpload}
/>
<CropModal
isOpen={isCropModalOpen}
imageBlob={imageBlob}
originalImageUrl={originalImageUrl}
setCropModalImageBlob={setCropModalImageBlob}
onClose={closeCropModal}
onSaveImageClick={handleCropModalSaveClick}
onDeleteClick={() => {
if (selectedVariantId) clearQuestionImages(question.id, selectedVariantId);
}}
cropAspectRatio={{ width: 300, height: 300 }}
<CropModalInit
originalImageUrl={variant?.originalImageUrl}
editedUrlImagesList={variant?.editedUrlImagesList}
questionId={question.id.toString()}
questionType={question.type}
quizId={quizQid}
variantId={variant?.id}
open={openCropModal}
selfClose={() => setOpenCropModal(false)}
setPictureUploading={setPictureUploading}
/>
<Box
sx={{
@ -152,9 +176,9 @@ export default function OptionsAndPicture({ question, setOpenBranchingPage }: Pr
)}
</Box>
</Box>
<ButtonsOptionsAndPict
<ButtonsOptions
switchState={switchState}
SSHC={setSwitchState}
setSwitchState={setSwitchState}
questionId={question.id}
questionContentId={question.content.id}
questionHasParent={question.content.rule.parentId?.length !== 0}

@ -1,3 +1,4 @@
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
import type { QuizQuestionVarImg, QuizQuestionVariant } from "@frontend/squzanswerer";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { updateQuestion } from "@root/questions/actions";
@ -9,16 +10,23 @@ type SettingOptionsAndPictProps = {
questionId: string;
replText: string;
isRequired: boolean;
isLargeCheck: boolean;
isOwn: boolean;
ownPlaceholder?: boolean;
isMulti?: boolean;
question: QuizQuestionVarImg;
};
const SettingOptionsAndPict = memo<SettingOptionsAndPictProps>(function ({ questionId, replText, isRequired, isOwn }) {
const SettingOptionsAndPict = memo<SettingOptionsAndPictProps>(function ({ question, questionId, ownPlaceholder, isMulti, isLargeCheck, replText, isRequired, isOwn }) {
const theme = useTheme();
const { switchOwn } = useAddAnswer();
const isWrappColumn = useMediaQuery(theme.breakpoints.down(980));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const isTablet = useMediaQuery(theme.breakpoints.down(985));
const isMobile = useMediaQuery(theme.breakpoints.down(680));
const setReplText = (replText: string) => {
updateQuestion(questionId, (question) => {
if (question.type !== "varimg") return;
@ -39,7 +47,7 @@ const SettingOptionsAndPict = memo<SettingOptionsAndPictProps>(function ({ quest
pt: isTablet ? "5px" : "0px",
}}
>
<Box
<Box
sx={{
pt: "20px",
display: "flex",
@ -49,7 +57,7 @@ const SettingOptionsAndPict = memo<SettingOptionsAndPictProps>(function ({ quest
width: "100%",
}}
>
{/* <Typography
<Typography
sx={{
height: isMobile ? "18px" : "auto",
fontWeight: "500",
@ -58,18 +66,27 @@ const SettingOptionsAndPict = memo<SettingOptionsAndPictProps>(function ({ quest
}}
>
Настройки ответов
</Typography> */}
{/* <CustomCheckbox
</Typography>
<CustomCheckbox
dataCy="checkbox-own-answer"
sx={{ mr: isMobile ? "0px" : "16px" }}
label={'Вариант "свой ответ"'}
checked={isOwn}
handleChange={({ target }) => {
switchOwn({ question, checked: target.checked })
}}
/>
<CustomCheckbox
dataCy="checkbox-long-text-answer"
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Многострочный ответ"}
checked={isLargeCheck}
handleChange={({ target }) => {
updateQuestion<QuizQuestionVariant>(questionId, (question) => {
question.content.own = target.checked;
question.content.largeCheck = target.checked;
});
}}
/> */}
/>
{!isWrappColumn && (
<Box sx={{ mt: isMobile ? "11px" : "6px", width: "100%" }}>
<Typography
@ -113,6 +130,7 @@ const SettingOptionsAndPict = memo<SettingOptionsAndPictProps>(function ({ quest
fontWeight: "500",
fontSize: "18px",
color: " #4D4D4D",
mt: isMobile ? "10px" : ""
}}
>
Настройки вопросов

@ -13,10 +13,13 @@ export default function SwitchOptionsAndPict({ switchState = "setting", question
case "setting":
return (
<SettingOptionsAndPict
question={question}
questionId={question.id}
replText={question.content.replText}
isRequired={question.content.required}
isOwn={question.content.own}
isLargeCheck={question.content.largeCheck}
isMulti={question.content.multi}
/>
);
case "help":

@ -1,70 +1,78 @@
import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
import { clearQuestionImages, uploadQuestionImage } from "@root/questions/actions";
import {
clearQuestionImages,
uploadQuestionImage,
} from "@root/questions/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
import { useState } from "react";
import { EnterIcon } from "../../../../assets/icons/questionsPage/enterIcon";
import type { QuizQuestionImages } from "@frontend/squzanswerer";
import { useAddAnswer } from "../../../../utils/hooks/useAddAnswer";
import { useDisclosure } from "../../../../utils/useDisclosure";
import { useMemo, useState } from "react";
import { EnterIcon } from "@/assets/icons/questionsPage/enterIcon";
import type { QuizQuestionVarImg } from "@frontend/squzanswerer/dist-package/model/questionTypes/varimg";
//@/model/questionTypes/images";
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
import { useDisclosure } from "@/utils/useDisclosure";
import { AnswerDraggableList } from "../../AnswerDraggableList";
import ImageEditAnswerItem from "../../AnswerDraggableList/ImageEditAnswerItem";
import ButtonsOptions from "../ButtonsOptions";
import ButtonsOptions from "../ButtonsLayout/ButtonsOptions";
import { UploadImageModal } from "../../UploadImage/UploadImageModal";
import SwitchAnswerOptionsPict from "./switchOptionsPict";
import imge from "@/assets/card-1.png"
import { CropModalInit } from "@/ui_kit/Modal/CropModal";
interface Props {
question: QuizQuestionImages;
question: QuizQuestionVarImg;
openBranchingPage: boolean;
setOpenBranchingPage: (a: boolean) => void;
}
export default function OptionsPicture({ question, openBranchingPage, setOpenBranchingPage }: Props) {
export default function OptionsPicture({
question,
openBranchingPage,
setOpenBranchingPage,
}: Props) {
const theme = useTheme();
const onClickAddAnAnswer = useAddAnswer();
const {onClickAddAnAnswer} = useAddAnswer();
const quizQid = useCurrentQuiz()?.qid;
const [pictureUploding, setPictureUploading] = useState<boolean>(false);
const [openCropModal, setOpenCropModal] = useState(false);
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
const variant = question.content.variants.find(
(variant) => variant.id === selectedVariantId,
)
const [switchState, setSwitchState] = useState("setting");
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
const { isCropModalOpen, openCropModal, closeCropModal, imageBlob, originalImageUrl, setCropModalImageBlob } =
useCropModalState();
const handleImageUpload = async (file: File) => {
if (!selectedVariantId) return;
setPictureUploading(true);
const url = await uploadQuestionImage(question.id, quizQid, file, (question, url) => {
if (!("variants" in question.content)) return;
const variant = question.content.variants.find((variant) => variant.id === selectedVariantId);
if (!variant) return;
variant.extendedText = url;
variant.originalImageUrl = url;
});
closeImageUploadModal();
openCropModal(file, url);
const url = await uploadQuestionImage(
question.id,
quizQid,
file,
(question, url) => {
if (!("variants" in question.content)) return;
const variant = question.content.variants.find(
(variant) => variant.id === selectedVariantId,
);
if (!variant) return;
variant.extendedText = url;
variant.originalImageUrl = url;
},
);
setOpenCropModal(true)
setPictureUploading(false);
};
function handleCropModalSaveClick(imageBlob: Blob) {
if (!selectedVariantId) return;
uploadQuestionImage(question.id, quizQid, imageBlob, (question, url) => {
if (!("variants" in question.content)) return;
const variant = question.content.variants.find((variant) => variant.id === selectedVariantId);
if (!variant) return;
variant.extendedText = url;
});
}
return (
<>
<Box sx={{ padding: "20px" }}>
@ -79,10 +87,12 @@ export default function OptionsPicture({ question, openBranchingPage, setOpenBra
largeCheck={question.content.largeCheck}
variant={variant}
isMobile={isMobile}
openCropModal={openCropModal}
openCropModal={() => {setOpenCropModal(true)}}
openImageUploadModal={openImageUploadModal}
pictureUploding={pictureUploding}
setSelectedVariantId={setSelectedVariantId}
isOwn={Boolean(variant?.isOwn)}
ownPlaceholder={question.content.ownPlaceholder}
/>
))}
/>
@ -91,17 +101,16 @@ export default function OptionsPicture({ question, openBranchingPage, setOpenBra
onClose={closeImageUploadModal}
handleImageChange={handleImageUpload}
/>
<CropModal
isOpen={isCropModalOpen}
imageBlob={imageBlob}
originalImageUrl={originalImageUrl}
setCropModalImageBlob={setCropModalImageBlob}
onClose={closeCropModal}
onSaveImageClick={handleCropModalSaveClick}
onDeleteClick={() => {
if (selectedVariantId) clearQuestionImages(question.id, selectedVariantId);
}}
cropAspectRatio={{ width: 452, height: 300 }}
<CropModalInit
originalImageUrl={variant?.originalImageUrl}
editedUrlImagesList={variant?.editedUrlImagesList}
questionId={question.id.toString()}
questionType={question.type}
quizId={quizQid}
variantId={variant?.id}
open={openCropModal}
selfClose={() => setOpenCropModal(false)}
setPictureUploading={setPictureUploading}
/>
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
<Link
@ -137,7 +146,7 @@ export default function OptionsPicture({ question, openBranchingPage, setOpenBra
</Box>
<ButtonsOptions
switchState={switchState}
SSHC={setSwitchState}
setSwitchState={setSwitchState}
questionId={question.id}
questionContentId={question.content.id}
questionType={question.type}

@ -0,0 +1,319 @@
import type { QuizQuestionImages, QuizQuestionVariant } from "@frontend/squzanswerer";
import { Box, Button, Typography, useMediaQuery, useTheme } from "@mui/material";
import { updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
import { memo } from "react";
import FormatIcon1 from "@/assets/icons/questionsPage/FormatIcon1";
import FormatIcon2 from "@/assets/icons/questionsPage/FormatIcon2";
import ProportionsIcon11 from "@/assets/icons/questionsPage/ProportionsIcon11";
import ProportionsIcon12 from "@/assets/icons/questionsPage/ProportionsIcon12";
import ProportionsIcon21 from "@/assets/icons/questionsPage/ProportionsIcon21";
import CustomTextField from "@ui_kit/CustomTextField";
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
type Proportion = "1:1" | "1:2" | "2:1";
type ProportionItem = {
value: Proportion;
icon: (props: { color: string }) => JSX.Element;
};
const PROPORTIONS: ProportionItem[] = [
{ value: "1:1", icon: ProportionsIcon11 },
{ value: "1:2", icon: ProportionsIcon21 },
{ value: "2:1", icon: ProportionsIcon12 },
];
type Format = "carousel" | "masonry";
type FormatItem = {
value: Format;
icon: (props: { color: string }) => JSX.Element;
};
const FORMATS: FormatItem[] = [
{ value: "carousel", icon: FormatIcon2 },
{ value: "masonry", icon: FormatIcon1 },
];
type SettingOptionsPictProps = {
question: QuizQuestionVariant;
questionId: string;
isRequired: boolean;
isMulti: boolean;
isOwn: boolean;
proportions: Proportion;
format: Format;
ownPlaceholder?: boolean;
isLargeCheck?: boolean;
};
const SettingOptionsPict = memo<SettingOptionsPictProps>(function ({
question,
questionId,
isRequired,
ownPlaceholder, isMulti, isLargeCheck,
isOwn,
proportions,
format,
}) {
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(985));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const setOwnPlaceholder = (replText: string) => {
updateQuestion(questionId, (question) => {
if (question.type !== "varimg") return;
question.content.ownPlaceholder = replText;
});
};
const {switchOwn} = useAddAnswer();
return (
<Box
sx={{
display: "flex",
justifyContent: "space-between",
flexDirection: isTablet ? "column" : null,
marginRight: isFigmaTablte ? (isMobile ? "0" : "0px") : "30px",
pb: "20px",
pl: "20px",
pt: isTablet ? "5px" : "0px",
}}
>
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
{/* <Box
sx={{
boxSizing: "border-box",
pt: "20px",
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>
<Box
sx={{
display: "flex",
gap: "10px",
}}
>
{PROPORTIONS.map((proportionItem, index) => (
<SelectIconButton
key={index}
Icon={proportionItem.icon}
isActive={proportionItem.value === proportions}
onClick={() => {
updateQuestion<QuizQuestionImages>(questionId, (question) => {
if (question.type !== "images") return;
question.content.xy = proportionItem.value;
});
}}
/>
))}
</Box>
</Box> */}
<Box
sx={{
boxSizing: "border-box",
pt: "20px",
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
dataCy="checkbox-long-text-answer"
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Многострочный ответ"}
checked={isLargeCheck}
handleChange={({ target }) => {
updateQuestion<QuizQuestionVariant>(questionId, (question) => {
question.content.largeCheck = target.checked;
});
}}
/> */}
<CustomCheckbox
dataCy="checkbox-multiple-answers"
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Можно несколько"}
checked={isMulti}
handleChange={({ target }) => {
updateQuestion<QuizQuestionVariant>(questionId, (question) => {
question.content.multi = target.checked;
});
}}
/>
<CustomCheckbox
dataCy="checkbox-own-answer"
sx={{ mr: isMobile ? "0px" : "16px" }}
label={'Вариант "свой ответ"'}
checked={isOwn}
handleChange={({ target }) => {
switchOwn({question, checked:target.checked})
}}
/>
{/* <Box sx={{ mt: isMobile ? "11px" : "6px", width: "100%" }}>
<Typography
sx={{
height: isMobile ? "18px" : "auto",
fontWeight: "500",
fontSize: "18px",
color: " #4D4D4D",
mb: "14px",
}}
>
Подсказка "своего ответа"
</Typography>
<CustomTextField
sx={{
maxWidth: "330px",
width: "100%",
mr: isMobile ? "0px" : "16px",
}}
maxLength={60}
placeholder={"мой ответ: три"}
value={ownPlaceholder}
onChange={({ target }) => setOwnPlaceholder(target.value)}
/>
</Box> */}
</Box>
</Box>
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
{/* <Box
sx={{
boxSizing: "border-box",
pt: "20px",
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>
<Box
sx={{
display: "flex",
gap: "10px",
}}
>
{FORMATS.map((formatItem, index) => (
<SelectIconButton
key={index}
Icon={formatItem.icon}
isActive={formatItem.value === format}
onClick={() => {
updateQuestion<QuizQuestionImages>(questionId, (question) => {
if (question.type !== "images") return;
question.content.format = formatItem.value;
});
}}
/>
))}
</Box>
</Box> */}
<Box
sx={{
pt: "20px",
pr: isFigmaTablte ? (isMobile ? "20px" : "0px") : "28px",
display: "flex",
flexDirection: "column",
gap: "14px",
width: "100%",
}}
>
<Typography sx={{ fontWeight: "500", fontSize: "18px", color: " #4D4D4D" }}>Настройки вопросов</Typography>
<CustomCheckbox
dataCy="checkbox-optional-question"
sx={{ alignItems: isMobile ? "flex-start" : "" }}
label={"Необязательный вопрос"}
checked={!isRequired}
handleChange={({ target }) =>
updateQuestion<QuizQuestionImages>(questionId, (question) => {
if (question.type !== "images") return;
question.content.required = !target.checked;
})
}
/>
</Box>
</Box>
</Box>
);
});
SettingOptionsPict.displayName = "SettingOptionsPict";
export default SettingOptionsPict;
interface Props {
Icon: (props: { color: string }) => JSX.Element;
isActive?: boolean;
onClick: () => void;
}
export function SelectIconButton({ Icon, isActive = false, onClick }: Props) {
const theme = useTheme();
return (
<Button
onClick={onClick}
variant="outlined"
startIcon={<Icon color={isActive ? theme.palette.navbarbg.main : theme.palette.brightPurple.main} />}
sx={{
backgroundColor: isActive ? theme.palette.brightPurple.main : "#eee4fc",
borderRadius: 0,
border: "none",
color: isActive ? theme.palette.brightPurple.main : theme.palette.grey2.main,
p: "7px",
width: "40px",
height: "40px",
minWidth: 0,
"& .MuiButton-startIcon": {
mr: 0,
ml: 0,
},
"&:hover": {
border: "none",
borderColor: isActive ? theme.palette.brightPurple.main : theme.palette.grey2.main,
},
}}
/>
);
}

@ -3,11 +3,13 @@ import { Box, Button, Typography, useMediaQuery, useTheme } from "@mui/material"
import { updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
import { memo } from "react";
import FormatIcon1 from "../../../../assets/icons/questionsPage/FormatIcon1";
import FormatIcon2 from "../../../../assets/icons/questionsPage/FormatIcon2";
import ProportionsIcon11 from "../../../../assets/icons/questionsPage/ProportionsIcon11";
import ProportionsIcon12 from "../../../../assets/icons/questionsPage/ProportionsIcon12";
import ProportionsIcon21 from "../../../../assets/icons/questionsPage/ProportionsIcon21";
import FormatIcon1 from "@/assets/icons/questionsPage/FormatIcon1";
import FormatIcon2 from "@/assets/icons/questionsPage/FormatIcon2";
import ProportionsIcon11 from "@/assets/icons/questionsPage/ProportionsIcon11";
import ProportionsIcon12 from "@/assets/icons/questionsPage/ProportionsIcon12";
import ProportionsIcon21 from "@/assets/icons/questionsPage/ProportionsIcon21";
import CustomTextField from "@ui_kit/CustomTextField";
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
type Proportion = "1:1" | "1:2" | "2:1";
@ -34,19 +36,23 @@ const FORMATS: FormatItem[] = [
{ value: "masonry", icon: FormatIcon1 },
];
type SettingOpytionsPictProps = {
type SettingOptionsPictProps = {
question: QuizQuestionVariant;
questionId: string;
isRequired: boolean;
isMulti: boolean;
isOwn: boolean;
proportions: Proportion;
format: Format;
ownPlaceholder?: boolean;
isLargeCheck?: boolean;
};
const SettingOptionsPict = memo<SettingOpytionsPictProps>(function ({
const SettingOptionsPict = memo<SettingOptionsPictProps>(function ({
question,
questionId,
isRequired,
isMulti,
ownPlaceholder, isMulti, isLargeCheck,
isOwn,
proportions,
format,
@ -55,6 +61,15 @@ const SettingOptionsPict = memo<SettingOpytionsPictProps>(function ({
const isTablet = useMediaQuery(theme.breakpoints.down(985));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const setOwnPlaceholder = (replText: string) => {
updateQuestion(questionId, (question) => {
if (question.type !== "varimg") return;
question.content.ownPlaceholder = replText;
});
};
const {switchOwn} = useAddAnswer();
return (
<Box
@ -68,8 +83,8 @@ const SettingOptionsPict = memo<SettingOpytionsPictProps>(function ({
pt: isTablet ? "5px" : "0px",
}}
>
{/* <Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
<Box
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
{/* <Box
sx={{
boxSizing: "border-box",
pt: "20px",
@ -110,7 +125,7 @@ const SettingOptionsPict = memo<SettingOpytionsPictProps>(function ({
/>
))}
</Box>
</Box>
</Box> */}
<Box
sx={{
boxSizing: "border-box",
@ -132,6 +147,17 @@ const SettingOptionsPict = memo<SettingOpytionsPictProps>(function ({
>
Настройки ответов
</Typography>
{/* <CustomCheckbox
dataCy="checkbox-long-text-answer"
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Многострочный ответ"}
checked={isLargeCheck}
handleChange={({ target }) => {
updateQuestion<QuizQuestionVariant>(questionId, (question) => {
question.content.largeCheck = target.checked;
});
}}
/> */}
<CustomCheckbox
dataCy="checkbox-multiple-answers"
sx={{ mr: isMobile ? "0px" : "16px" }}
@ -149,13 +175,35 @@ const SettingOptionsPict = memo<SettingOpytionsPictProps>(function ({
label={'Вариант "свой ответ"'}
checked={isOwn}
handleChange={({ target }) => {
updateQuestion<QuizQuestionVariant>(questionId, (question) => {
question.content.own = target.checked;
});
switchOwn({question, checked:target.checked})
}}
/>
{/* <Box sx={{ mt: isMobile ? "11px" : "6px", width: "100%" }}>
<Typography
sx={{
height: isMobile ? "18px" : "auto",
fontWeight: "500",
fontSize: "18px",
color: " #4D4D4D",
mb: "14px",
}}
>
Подсказка "своего ответа"
</Typography>
<CustomTextField
sx={{
maxWidth: "330px",
width: "100%",
mr: isMobile ? "0px" : "16px",
}}
maxLength={60}
placeholder={"мой ответ: три"}
value={ownPlaceholder}
onChange={({ target }) => setOwnPlaceholder(target.value)}
/>
</Box> */}
</Box>
</Box> */}
</Box>
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
{/* <Box
sx={{

@ -1,6 +1,6 @@
import { QuizQuestionImages } from "@frontend/squzanswerer";
import HelpQuestions from "../../helpQuestions";
import SettingOpytionsPict from "./settingOpytionsPict";
import SettingOptionsPict from "./settingOptionsPict";
interface Props {
switchState: string;
@ -11,13 +11,17 @@ export default function SwitchAnswerOptionsPict({ switchState = "setting", quest
switch (switchState) {
case "setting":
return (
<SettingOpytionsPict
<SettingOptionsPict
question={question}
questionId={question.id}
isRequired={question.content.required}
isMulti={question.content.multi}
isOwn={question.content.own}
proportions={question.content.xy}
format={question.content.format}
ownPlaceholder={question.content.ownPlaceholder}
isLargeCheck={question.content.isLargeCheck}
/>
);
case "help":

@ -9,7 +9,7 @@ import LightbulbIcon from "@/assets/icons/questionsPage/lightbulbIcon";
import LikeIcon from "@/assets/icons/questionsPage/likeIcon";
import TropfyIcon from "@/assets/icons/questionsPage/tropfyIcon";
import type { QuizQuestionRating } from "@frontend/squzanswerer";
import ButtonsOptions from "../ButtonsOptions";
import ButtonsOptions from "../ButtonsLayout/ButtonsOptions";
import SwitchRating from "./switchRating";
const TextField = MuiTextField as unknown as FC<TextFieldProps>;
@ -289,7 +289,7 @@ export default function RatingOptions({ question, openBranchingPage, setOpenBran
</Box>
<ButtonsOptions
switchState={switchState}
SSHC={setSwitchState}
setSwitchState={setSwitchState}
questionId={question.id}
questionContentId={question.content.id}
questionType={question.type}

@ -4,7 +4,7 @@ import { useDebouncedCallback } from "use-debounce";
import CustomNumberField from "@ui_kit/CustomNumberField";
import ButtonsOptions from "../ButtonsOptions";
import ButtonsOptions from "../ButtonsLayout/ButtonsOptions";
import SwitchSlider from "./switchSlider";
import { updateQuestion } from "@root/questions/actions";
@ -209,7 +209,7 @@ export default function SliderOptions({ question, openBranchingPage, setOpenBran
</Box>
<ButtonsOptions
switchState={switchState}
SSHC={setSwitchState}
setSwitchState={setSwitchState}
questionId={question.id}
questionContentId={question.content.id}
questionType={question.type}

@ -1,11 +1,11 @@
import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useEffect, useState } from "react";
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
import { EnterIcon } from "@/assets/icons/questionsPage/enterIcon";
import type { QuizQuestionVariant } from "@frontend/squzanswerer";
import { useAddAnswer } from "../../../utils/hooks/useAddAnswer";
import { AnswerDraggableList } from "../AnswerDraggableList";
import AnswerItem from "../AnswerDraggableList/AnswerItem";
import ButtonsOptionsAndPict from "../QuestionOptions/ButtonsOptionsAndPict";
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
import { AnswerDraggableList } from "../../AnswerDraggableList";
import AnswerItem from "../../AnswerDraggableList/AnswerItem";
import ButtonsOptions from "../ButtonsLayout/ButtonsOptions";
import SwitchAnswerOptions from "./switchAnswerOptions";
interface Props {
@ -15,7 +15,7 @@ interface Props {
}
export default function AnswerOptions({ question, openBranchingPage, setOpenBranchingPage }: Props) {
const onClickAddAnAnswer = useAddAnswer();
const {onClickAddAnAnswer} = useAddAnswer();
const [switchState, setSwitchState] = useState("setting");
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
@ -44,13 +44,17 @@ export default function AnswerOptions({ question, openBranchingPage, setOpenBran
) : (
<AnswerDraggableList
questionId={question.id}
variants={question.content.variants.map((variant, index) => (
variants={question.content.variants
.filter(variant => !variant.isOwn ? true : question.content.own && variant.isOwn)
.map((variant, index) => (
<AnswerItem
key={variant.id}
index={index}
disableKeyDown={question.content.variants.length >= 100}
questionId={question.id}
variant={variant}
isOwn={Boolean(variant.isOwn)}
ownPlaceholder={question.content.ownPlaceholder}
/>
))}
/>
@ -100,9 +104,9 @@ export default function AnswerOptions({ question, openBranchingPage, setOpenBran
)}
</Box>
</Box>
<ButtonsOptionsAndPict
<ButtonsOptions
switchState={switchState}
SSHC={setSwitchState}
setSwitchState={setSwitchState}
questionId={question.id}
questionContentId={question.content.id}
questionHasParent={question.content.rule.parentId?.length !== 0}

@ -3,20 +3,25 @@ import { updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
import type { QuizQuestionVariant } from "@frontend/squzanswerer";
import { memo } from "react";
import CustomTextField from "@ui_kit/CustomTextField";
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
interface Props {
question: QuizQuestionVariant;
questionId: string;
isRequired: boolean;
isLargeCheck: boolean;
isMulti: boolean;
isOwn: boolean;
ownPlaceholder?: string;
}
const ResponseSettings = memo<Props>(function ({ questionId, isRequired, isLargeCheck, isMulti, isOwn }) {
const ResponseSettings = memo<Props>(function ({question, questionId, ownPlaceholder, isRequired, isLargeCheck, isMulti, isOwn }) {
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 {switchOwn} = useAddAnswer();
return (
<Box
@ -30,7 +35,7 @@ const ResponseSettings = memo<Props>(function ({ questionId, isRequired, isLarge
pt: isTablet ? "5px" : "0px",
}}
>
{/* <Box
<Box
sx={{
boxSizing: "border-box",
pt: "20px",
@ -54,7 +59,7 @@ const ResponseSettings = memo<Props>(function ({ questionId, isRequired, isLarge
<CustomCheckbox
dataCy="checkbox-long-text-answer"
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Длинный текстовый ответ"}
label={"Многострочный ответ"}
checked={isLargeCheck}
handleChange={({ target }) => {
updateQuestion<QuizQuestionVariant>(questionId, (question) => {
@ -79,12 +84,10 @@ const ResponseSettings = memo<Props>(function ({ questionId, isRequired, isLarge
label={'Вариант "свой ответ"'}
checked={isOwn}
handleChange={({ target }) => {
updateQuestion<QuizQuestionVariant>(questionId, (question) => {
question.content.own = target.checked;
});
switchOwn({question, checked:target.checked})
}}
/>
</Box> */}
</Box>
<Box
sx={{
boxSizing: "border-box",

@ -1,6 +1,6 @@
import { QuizQuestionVariant } from "@frontend/squzanswerer";
import UploadImage from "../UploadImage";
import HelpQuestions from "../helpQuestions";
import UploadImage from "../../UploadImage";
import HelpQuestions from "../../helpQuestions";
import ResponseSettings from "./responseSettings";
interface Props {
@ -13,11 +13,13 @@ export default function SwitchAnswerOptions({ switchState = "setting", question
case "setting":
return (
<ResponseSettings
question={question}
questionId={question.id}
isRequired={question.content.required}
isLargeCheck={question.content.largeCheck}
isMulti={question.content.multi}
isOwn={question.content.own}
isOwnPlaceholder={question.content.ownPlaceholder}
/>
);
case "help":

@ -20,10 +20,10 @@ import { useUiTools } from "@root/uiTools/store";
import QuizPreview from "@ui_kit/QuizPreview/QuizPreview";
import { useLayoutEffect } from "react";
import { createPortal } from "react-dom";
import AddPlus from "../../assets/icons/questionsPage/addPlus";
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
import AddPlus from "@/assets/icons/questionsPage/addPlus";
import ArrowLeft from "@/assets/icons/questionsPage/arrowLeft";
import BranchingQuestions from "./Branching/BranchingModal/BranchingQuestionsModal";
import { QuestionSwitchWindowTool } from "./QuestionSwitchWindowTool";
import { QuestionSwitchWindowTool } from "./Branching/QuestionSwitchWindowTool";
interface Props {
openBranchingPage: boolean;

@ -1,4 +1,4 @@
import DataOptions from "./QuestionOptions/DataOptions/DataOptions";
import DateOptions from "./QuestionOptions/DateOptions/DateOptions";
import DropDown from "./DropDown/DropDown";
import Emoji from "./Emoji/Emoji";
import OptionsAndPicture from "./QuestionOptions/OptionsAndPicture/OptionsAndPicture";
@ -8,7 +8,7 @@ import PageOptions from "./QuestionOptions/PageOptions/PageOptions";
import RatingOptions from "./QuestionOptions/RatingOptions/RatingOptions";
import SliderOptions from "./QuestionOptions/SliderOptions/SliderOptions";
import UploadFile from "./UploadFile/UploadFile";
import AnswerOptions from "./answerOptions/AnswerOptions";
import AnswerOptions from "./QuestionOptions/answerOptions/AnswerOptions";
import { notReachable } from "../../utils/notReachable";
import { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
@ -76,7 +76,7 @@ export default function SwitchQuestionsPage({ question, openBranchingPage, setOp
case "date":
return (
<DataOptions
<DateOptions
question={question}
openBranchingPage={openBranchingPage}
setOpenBranchingPage={setOpenBranchingPage}

@ -2,17 +2,17 @@ import { QuestionType } from "@model/question/question";
import { Box } from "@mui/material";
import { createTypedQuestion } from "@root/questions/actions";
import QuestionsMiniButton from "@ui_kit/QuestionsMiniButton";
import Answer from "../../assets/icons/questionsPage/answer";
import Date from "../../assets/icons/questionsPage/date";
import Download from "../../assets/icons/questionsPage/download";
import DropDown from "../../assets/icons/questionsPage/drop_down";
import Emoji from "../../assets/icons/questionsPage/emoji";
import Input from "../../assets/icons/questionsPage/input";
import OptionsAndPict from "../../assets/icons/questionsPage/options_and_pict";
import OptionsPict from "../../assets/icons/questionsPage/options_pict";
import Page from "../../assets/icons/questionsPage/page";
import RatingIcon from "../../assets/icons/questionsPage/rating";
import Slider from "../../assets/icons/questionsPage/slider";
import Answer from "@/assets/icons/questionsPage/answer";
import Date from "@/assets/icons/questionsPage/date";
import Download from "@/assets/icons/questionsPage/download";
import DropDown from "@/assets/icons/questionsPage/drop_down";
import Emoji from "@/assets/icons/questionsPage/emoji";
import Input from "@/assets/icons/questionsPage/input";
import OptionsAndPict from "@/assets/icons/questionsPage/options_and_pict";
import OptionsPict from "@/assets/icons/questionsPage/options_pict";
import Page from "@/assets/icons/questionsPage/page";
import RatingIcon from "@/assets/icons/questionsPage/rating";
import Slider from "@/assets/icons/questionsPage/slider";
import type { UntypedQuizQuestion } from "../../model/questionTypes/shared";
interface Props {

@ -11,9 +11,9 @@ import {
} from "@mui/material";
import { updateQuestion } from "@root/questions/actions";
import { useEffect, useState } from "react";
import ArrowDown from "../../../assets/icons/ArrowDownIcon";
import InfoIcon from "../../../assets/icons/InfoIcon";
import ButtonsOptions from "../QuestionOptions/ButtonsOptions";
import ArrowDown from "@/assets/icons/ArrowDownIcon";
import InfoIcon from "@/assets/icons/InfoIcon";
import ButtonsOptions from "../QuestionOptions/ButtonsLayout/ButtonsOptions";
import SwitchUpload from "./switchUpload";
import TooltipClickInfo from "@ui_kit/Toolbars/TooltipClickInfo";
import { QuizQuestionFile, UploadFileType } from "@frontend/squzanswerer";
@ -187,7 +187,7 @@ export default function UploadFile({ question, openBranchingPage, setOpenBranchi
</Box>
<ButtonsOptions
switchState={switchState}
SSHC={setSwitchState}
setSwitchState={setSwitchState}
questionId={question.id}
questionContentId={question.content.id}
questionType={question.type}

@ -8,9 +8,9 @@ import {
InputAdornment,
useMediaQuery,
} from "@mui/material";
import UploadIcon from "../../../assets/icons/UploadIcon";
import SearchIcon from "../../../assets/icons/SearchIcon";
import UnsplashIcon from "../../../assets/icons/Unsplash.svg";
import UploadIcon from "@/assets/icons/UploadIcon";
import SearchIcon from "@/assets/icons/SearchIcon";
import UnsplashIcon from "@/assets/icons/Unsplash.svg";
import { useRef, useState, type DragEvent } from "react";
type ImageFormat = "jpg" | "jpeg" | "png" | "gif";

@ -4,6 +4,7 @@ import { updateQuestion, uploadQuestionImage } from "@root/questions/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { useState } from "react";
import { DropZone } from "../../../pages/startPage/dropZone";
import { CropModalInit } from "@/ui_kit/Modal/CropModal";
type UploadImageProps = {
question: AnyTypedQuizQuestion;
@ -15,6 +16,8 @@ type UploadImageProps = {
export default function UploadImage({ question, cropAspectRatio }: UploadImageProps) {
const [pictureUploding, setPictureUploading] = useState<boolean>(false);
const [openCropModal, setOpenCropModal] = useState(false);
const theme = useTheme();
const quiz = useCurrentQuiz();
@ -33,43 +36,53 @@ export default function UploadImage({ question, cropAspectRatio }: UploadImagePr
Загрузить изображение
</Typography>
{pictureUploding ? (
<Skeleton
variant="rounded"
sx={{ height: "120px", width: "300px" }}
/>
<Skeleton variant="rounded" sx={{ height: "120px", width: "300px" }} />
) : (
<DropZone
text={"5 MB максимум"}
sx={{ maxWidth: "300px", width: "100%" }}
cropAspectRatio={cropAspectRatio}
imageUrl={question.content.back}
imageUrl={question.content.originalBack}
originalImageUrl={question.content.originalBack}
onImageUploadClick={async (file) => {
setPictureUploading(true);
await uploadQuestionImage(question.id, quiz.qid, file, (question, url) => {
question.content.back = url;
question.content.originalBack = url;
});
await uploadQuestionImage(
question.id,
quiz.qid,
file,
(question, url) => {
question.content.back = url;
question.content.originalBack = url;
},
);
setOpenCropModal(true)
setPictureUploading(false);
}}
}
}
onDeleteClick={() => {
updateQuestion(question.id, (question) => {
question.content.back = null;
question.content.originalBack = null;
if ("editedUrlImagesList" in question.content) question.content.editedUrlImagesList = null;
});
}}
onImageSaveClick={async (file) => {
setPictureUploading(true);
await uploadQuestionImage(question.id, quiz.qid, file, (question, url) => {
question.content.back = url;
});
setPictureUploading(false);
onImageSavedClick={() => {
setOpenCropModal(true)
}}
/>
)}
<CropModalInit
originalImageUrl={question.content.originalBack}
editedUrlImagesList={question.content?.editedUrlImagesList}
questionId={question.id.toString()}
questionType={question.type}
quizId={quiz.qid}
open={openCropModal}
selfClose={() => setOpenCropModal(false)}
setPictureUploading={setPictureUploading}
/>
</Box>
);
}

@ -2,7 +2,7 @@ import { Box, Button, ButtonBase, Dialog, Typography, useTheme } from "@mui/mate
import CustomTextField from "@ui_kit/CustomTextField";
import SelectableButton from "@ui_kit/SelectableButton";
import { useState } from "react";
import UploadIcon from "../../assets/icons/UploadIcon";
import UploadIcon from "@/assets/icons/UploadIcon";
import type { DragEvent } from "react";
type BackgroundTypeModal = "linkVideo" | "ownVideo";

@ -4,7 +4,7 @@ import CustomTextField from "@ui_kit/CustomTextField";
import SelectableButton from "@ui_kit/SelectableButton";
import UploadBox from "@ui_kit/UploadBox";
import { memo, useState } from "react";
import UploadIcon from "../../assets/icons/UploadIcon";
import UploadIcon from "@/assets/icons/UploadIcon";
import UploadVideoModal from "./UploadVideoModal";
type BackgroundType = "text" | "video";

@ -1,7 +1,7 @@
import { FC } from "react";
import { Box, Typography } from "@mui/material";
import { DateDefinition, TimeDefinition } from "./helper";
import { CardAnswer } from "./CardAnswer";
import { CardAnswer } from "./cardAnswers/CardAnswer";
import { Result } from "@root/results/store";
interface AnswerListProps {

@ -1,9 +1,9 @@
import { ArrowDownIcon } from "@icons/questionsPage/ArrowDownIcon";
import { Box, IconButton, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
import { FC, MouseEvent, useState } from "react";
import { ContactIcon } from "./icons/ContactIcon";
import { MessageIcon } from "./icons/MessageIcon";
import { PhoneIcon } from "./icons/PhoneIcon";
import { ContactIcon } from "../icons/ContactIcon";
import { MessageIcon } from "../icons/MessageIcon";
import { PhoneIcon } from "../icons/PhoneIcon";
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import { deleteResult, obsolescenceResult } from "@root/results/actions";
@ -12,10 +12,14 @@ import { useQuizStore } from "@root/quizes/store";
import { useQuestionsStore } from "@root/questions/store";
import AddressIcon from "@icons/ContactFormIcon/AddressIcon";
import { DeleteModal } from "./DeleteModal";
import { DeleteModal } from "../DeleteModal";
import type { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
import { useCurrentQuiz } from "@/stores/quizes/hooks";
import { ListOfOptionsCardAnswer } from "./ListOfOptionsCardAnswer";
import { ListOfImagesCardAnswer } from "./ListOfImagesCardAnswer";
import { timewebContentFile, timewebContent } from "./helper"
interface CardAnswerProps {
isNew: boolean;
@ -312,6 +316,26 @@ export const CardAnswer: FC<CardAnswerProps> = ({
qid = quest[i].backendId
}
}
if (answer?.Version !== undefined) {
if (typeOuestion === "variant" || typeOuestion === "emoji") return (
<ListOfOptionsCardAnswer
title={titleQuestion || ""}
answer={answer.content}
id={id}
/>
)
if (typeOuestion === "varimg" || typeOuestion === "images" && answer.content.includes("Image")) return (
<ListOfImagesCardAnswer
title={titleQuestion || ""}
answer={answer.content}
id={id}
quizId={quiz?.id}
questionId={qid}
/>
)
}
return (
<Box
key={answer.id}
@ -325,7 +349,7 @@ export const CardAnswer: FC<CardAnswerProps> = ({
<Typography sx={{ fontSize: "18px", color: "#9A9AAF" }}>
{id + 1}. {titleQuestion}.
</Typography>
{typeOuestion === "file" && (
{typeOuestion === "file" && answer.content && (
<Link
download
href={timewebContentFile(quiz?.qid, answer.content, qid)}
@ -366,6 +390,7 @@ export const CardAnswer: FC<CardAnswerProps> = ({
{!(typeOuestion === "file" || typeOuestion === "images" || typeOuestion === "varimg") && (
<Typography sx={{ fontSize: "18px" }}>{answer.content}</Typography>
)}
</Box>
);
})}
@ -433,26 +458,3 @@ export const CardAnswer: FC<CardAnswerProps> = ({
</>
);
};
function timewebContentFile(editQuizId: string, content: string, qid: string) {
if (content.includes("<img")) {
//Старая версия: контент лежит в теге
//<img style="width:100%; max-width:250px; max-height:250px" src="https://s3.timeweb.cloud/3c580be9-30c7959dc17b/squizimages/03520c507b35e8/cq39gn7o73evdd30"/>
return content.split("<")[1].split('src="')[1].split('"/>')[0]
}
//Новая версия: контент просто записан с указанием расширения файла
return `https://s3.timeweb.cloud/3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b/squizanswer/${editQuizId}/${qid}/${content}`
}
function timewebContent(editQuizId: string, content: string, qid: string) {
if (content.includes("<img")) {
//Старая версия: контент лежит в теге
//<img style="width:100%; max-width:250px; max-height:250px" src="https://s3.timeweb.cloud/3c580be9-30c7959dc17b/squizimages/03520c507b35e8/cq39gn7o73evdd30"/>
return content.split("<")[1].split('src="')[1].split('"/>')[0]
}
if (content.includes(`"Image"`)) {
const data = JSON.parse(content)
return data.Image
}
//Новая версия: контент просто записан с указанием расширения файла(устарело)
return `https://s3.timeweb.cloud/3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b/squizimages/${editQuizId}/${content}`
}

@ -0,0 +1,50 @@
import { Box, Typography } from "@mui/material";
import { splitUserText, timewebContent } from "./helper";
interface Props {
title: string;
id: number;
answer: string;
quizId: string;
questionId: string;
}
export const ListOfImagesCardAnswer = ({
title,
id,
answer,
quizId,
questionId,
}: Props) => {
return <Box
sx={{
display: "flex",
alignItems: "start",
gap: "13px",
}}
onClick={(e) => e.stopPropagation()}
>
<Typography sx={{ fontSize: "18px", color: "#9A9AAF" }}>
{id + 1}. {title}.
</Typography>
<Box>
{
splitUserText(answer)
.filter(text => text.length)
.map(text => {
const { Image, Description } = JSON.parse(text)
return (<>
<img
width={40}
height={40}
src={timewebContent(quizId, Image, questionId)}
/>
<Typography sx={{ fontSize: "18px", wordBreak: "break-word" }}>{Description}</Typography>
</>)
})
}
</Box>
</Box>
};

@ -0,0 +1,37 @@
import { Box, Typography } from "@mui/material";
import { splitUserText } from "./helper";
interface Props {
title: string;
id: number;
answer: string;
}
export const ListOfOptionsCardAnswer = ({
title,
id,
answer
}: Props) => {
return <Box
sx={{
display: "flex",
alignItems: "start",
gap: "13px",
}}
onClick={(e) => e.stopPropagation()}
>
<Typography sx={{ fontSize: "18px", color: "#9A9AAF" }}>
{id + 1}. {title}.
</Typography>
<Box>
{
splitUserText(answer)
.filter(text => text.length)
.map(text => <Typography sx={{ fontSize: "18px", wordBreak: "break-word" }}>{text}</Typography>)
}
</Box>
</Box>
};

@ -0,0 +1,38 @@
export function splitUserText(input: string) {
// Регулярное выражение для поиска текста в обратных кавычках
const regex = /`([^`]*)`/g; // Изменено на ([^]*) для захвата пустых строк
let result = [];
let match;
// Найти все совпадения
while ((match = regex.exec(input)) !== null) {
// Добавляем найденный текст (включая пустые строки) в массив
result.push(match[1]);
}
return result;
}
export function timewebContentFile(editQuizId: string, content: string, qid: string) {
if (content.includes("<img")) {
//Старая версия: контент лежит в теге
//<img style="width:100%; max-width:250px; max-height:250px" src="https://s3.timeweb.cloud/3c580be9-30c7959dc17b/squizimages/03520c507b35e8/cq39gn7o73evdd30"/>
return content.split("<")[1].split('src="')[1].split('"/>')[0]
}
//Новая версия: контент просто записан с указанием расширения файла
return `https://s3.timeweb.cloud/3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b/squizanswer/${editQuizId}/${qid}/${content}`
}
export function timewebContent(editQuizId: string, content: string, qid: string) {
if (content.includes("<img")) {
//Старая версия: контент лежит в теге
//<img style="width:100%; max-width:250px; max-height:250px" src="https://s3.timeweb.cloud/3c580be9-30c7959dc17b/squizimages/03520c507b35e8/cq39gn7o73evdd30"/>
return content.split("<")[1].split('src="')[1].split('"/>')[0]
} else if (content.includes(`"Image"`)) {
const data = JSON.parse(content)
return data.Image
} else {
return content
}
//Новая версия: контент просто записан с указанием расширения файла(устарело)
return `https://s3.timeweb.cloud/3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b/squizimages/${editQuizId}/${content}`
}

@ -10,10 +10,10 @@ import StarIconPoints from "./StarIconsPoints";
interface Props {
switchState: string;
SSHC: (data: string) => void;
setSwitchState: (data: string) => void;
}
export default function ButtonsOptionsForm({ SSHC, switchState }: Props) {
export default function ButtonsOptionsForm({ setSwitchState, switchState }: Props) {
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(800));
@ -74,7 +74,7 @@ export default function ButtonsOptionsForm({ SSHC, switchState }: Props) {
<MiniButtonSetting
key={index}
onClick={() => {
SSHC(value);
setSwitchState(value);
}}
sx={{
backgroundColor:

@ -24,7 +24,7 @@ export const DescriptionForm = () => {
setPriceButtonsType(type);
};
const SSHC = (data: string) => {
const setSwitchState = (data: string) => {
setSwitchState(data);
};
@ -188,7 +188,7 @@ export const DescriptionForm = () => {
</Button>
)}
</Box>
<ButtonsOptionsForm switchState={switchState} SSHC={SSHC} />
<ButtonsOptionsForm switchState={switchState} setSwitchState={setSwitchState} />
<SwitchResult switchState={switchState} totalIndex={0} />
</Box>
);

@ -5,7 +5,7 @@ import {
useMediaQuery,
Button,
} from "@mui/material";
import image from "../../assets/Rectangle 110.png";
import image from "@/assets/Rectangle 110.png";
export const FirstEntry = () => {
const theme = useTheme();

@ -1,8 +1,8 @@
import { Box, Button, Tooltip, useMediaQuery, useTheme } from "@mui/material";
import { updateQuiz } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import image from "../../assets/Rectangle 110.png";
import Info from "../../assets/icons/Info";
import image from "@/assets/Rectangle 110.png";
import Info from "@/assets/icons/Info";
import TooltipClickInfo from "@ui_kit/Toolbars/TooltipClickInfo";
export const Result = () => {

@ -53,8 +53,8 @@ export default function AvailablePrivilege() {
}
const quizUnlimDays = getCramps(quizUnlimTime, userPrivileges?.quizUnlimTime?.created_at || "");
const squizBadgeDays = getCramps(squizHideBadge, userPrivileges?.squizHideBadge?.created_at || "");
const currentDate = moment();
console.log(userPrivileges)
console.log(quizUnlimTime)
return (
<Box

@ -145,15 +145,15 @@ export default function EditPage({
{quizConfig && (
<>
<Stepper activeStep={currentStep} />
<SwitchStepPages
activeStep={currentStep}
quizType={quizConfig.type}
quizResults={quizConfig.results}
quizStartPageType={quizConfig.startpageType}
openBranchingPage={openBranchingPage}
setOpenBranchingPage={setOpenBranchingPage}
widthMain={widthMain}
/>
<SwitchStepPages
activeStep={currentStep}
quizType={quizConfig.type}
quizResults={quizConfig.results}
quizStartPageType={quizConfig.startpageType}
openBranchingPage={openBranchingPage}
setOpenBranchingPage={setOpenBranchingPage}
widthMain={widthMain}
/>
</>
)}
</Box>

@ -15,6 +15,7 @@ import {
import CloseIcon from "@mui/icons-material/Close";
import * as React from "react";
import { useState } from "react";
import { wrap } from "module";
export default function ModalSizeImage() {
const theme = useTheme();
@ -25,27 +26,44 @@ export default function ModalSizeImage() {
function createData(name: string, size: string) {
return { name, size };
}
const rows = [
createData("Прямая ссылка/домен", "1792х1509 px"),
createData("Модальное окно на сайте", "1380х1300 px"),
createData("Во ВКонтакте", "1166х1200 px"),
createData("Версия для планшета", "767х220 px"),
createData("Мобильная версия", "400х220 px"),
createData("Картинка для дизайна Centered", "900х490 px"),
createData("Картинка для дизайна Expanded", "1920х1080 px"),
createData("Стартовая \"Centered\" (десктоп)", "844х306 px"),
createData("Стартовая \"Centered\" (планшет гориз.)", "844х530 px"),
createData("Стартовая \"Centered\" (планшет верт.)", "660х260 px"),
createData("Логотип", "110 х 40 px"),
createData("\"Варианты с картинками\" (десктоп)", "317х257 px"),
createData("\"Варианты с картинками\" (планшет)", "455х257 px"),
createData("\"Варианты с картинками\" (мобилка)", "160х183 px"),
createData("\"Варианты и картинка\" (десктоп)", "450х450 px"),
createData("\"Варианты и картинка\" (мобилка)", "335х335 px"),
createData("\"Своё поле для ввода\" (десктоп)", "450х450 px"),
createData("\"Своё поле для ввода\" (мобилка)", "335х335 px"),
createData("\"Варианты\" (десктоп)", "450х450 px"),
createData("\"Варианты\" (мобилка)", "335х335 px"),
createData("Картинка для результата (десктоп)", "700х306 px"),
createData("Картинка для результата (мобилка)", "335х236 px"),
];
// const rows = [
// createData("Прямая ссылка/домен", "1792х1509 px"),
// createData("Модальное окно на сайте", "1380х1300 px"),
// createData("Во ВКонтакте", "1166х1200 px"),
// createData("Версия для планшета", "767х220 px"),
// createData("Мобильная версия", "400х220 px"),
// createData("Картинка для дизайна Centered", "900х490 px"),
// createData("Картинка для дизайна Expanded", "1920х1080 px"),
// ];
const rows2 = [
createData("Вертикальный вариант", "180х240 px"),
createData("Квадратные", "240х240 px"),
createData("Варианты и картинка", "380х307 px"),
createData("Консультант", "140х140 px"),
createData("Логотип", "107х37 px"),
createData("Результаты", "1100х600 px"),
createData("Бонус", "200х60 px"),
createData('Картинка для формата вопроса "Страница"', "860х1250 px"),
];
// const rows2 = [
// createData("Вертикальный вариант", "180х240 px"),
// createData("Квадратные", "240х240 px"),
// createData("Варианты и картинка", "380х307 px"),
// createData("Консультант", "140х140 px"),
// createData("Логотип", "107х37 px"),
// createData("Результаты", "1100х600 px"),
// createData("Бонус", "200х60 px"),
// createData('Картинка для формата вопроса "Страница"', "860х1250 px"),
// ];
return (
<>
@ -84,81 +102,87 @@ export default function ModalSizeImage() {
p: 0,
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "baseline",
backgroundColor: theme.palette.background.default,
padding: "10px 9px 17px 20px",
borderRadius: "12px 12px 0px 0px",
}}
>
<Typography variant={"h5"}>Размеры картинок</Typography>
<IconButton onClick={handleClose}>
<CloseIcon />
</IconButton>
</Box>
<Box sx={{ padding: "15px 20px 0px" }}>
<Typography
variant={"body2"}
sx={{ color: theme.palette.grey2.main, fontWeight: 400 }}
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "baseline",
backgroundColor: theme.palette.background.default,
padding: "10px 9px 17px 20px",
borderRadius: "12px 12px 0px 0px",
}}
>
Рекомендованный размер зависит от того, как вы будете чаще
использовать quiz:
</Typography>
</Box>
<Box sx={{ padding: "15px 40px 30px" }}>
{rows.map(({ name, size }, index) => (
<Box
key={name || index}
sx={{
display: "flex",
justifyContent: "space-between",
gap: "6px",
position: "relative",
width: "100%",
paddingBottom: "5px",
}}
>
<Box
sx={{
position: "absolute",
top: 18,
left: 0,
right: 0,
borderBottom: "solid 1px #F2F3F7",
}}
/>
<Box
sx={{
display: "block ruby",
position: "relative",
zIndex: 1,
background: "white",
}}
>
<Typography variant={"body2"} fontWeight={400}>
{name}
</Typography>
</Box>
<Box
sx={{
display: "block ruby",
position: "relative",
zIndex: 1,
background: "white",
}}
>
<Typography sx={{ whiteSpace: "nowrap" }} variant={"body2"}>
{size}
</Typography>
</Box>
</Box>
))}
</Box>
<Typography variant={"h5"}>Размеры картинок</Typography>
<IconButton onClick={handleClose}>
<CloseIcon />
</IconButton>
</Box>
<Box
overflow="auto"
height="auto"
maxHeight="650px"
>
<Box sx={{ padding: "15px 20px 0px" }}>
<Typography
variant={"body2"}
sx={{ color: theme.palette.grey2.main, fontWeight: 400 }}
>
Рекомендованный размер зависит от того, как вы будете чаще
использовать quiz:
</Typography>
</Box>
<Box sx={{ padding: "15px 40px 30px" }}>
{rows.map(({ name, size }, index) => (
<Box
key={name || index}
sx={{
display: "flex",
justifyContent: "space-between",
gap: "6px",
position: "relative",
width: "100%",
paddingBottom: "5px",
flexWrap: "wrap"
}}
>
<Box
sx={{
position: "absolute",
top: 18,
left: 0,
right: 0,
borderBottom: "solid 1px #F2F3F7",
}}
/>
<Box
sx={{
display: "block ruby",
position: "relative",
zIndex: 1,
background: "white",
}}
>
<Typography variant={"body2"} fontWeight={400}>
{name}
</Typography>
</Box>
<Box
sx={{
display: "block ruby",
position: "relative",
zIndex: 1,
background: "white",
}}
>
<Typography sx={{ whiteSpace: "nowrap" }} variant={"body2"}>
{size}
</Typography>
</Box>
</Box>
))}
</Box>
{/* <Box
sx={{
backgroundColor: theme.palette.background.default,
padding: "20px",
@ -214,7 +238,9 @@ export default function ModalSizeImage() {
</Box>
</Box>
))}
</Box> */}
</Box>
</Box>
</Modal>
</>

@ -9,7 +9,7 @@ import LayoutStandartIcon from "@icons/LayoutStandartIcon";
import { QuizStartpageType } from "@model/quizSettings";
import InfoIcon from "@icons/InfoIcon";
import UploadBox from "@ui_kit/UploadBox";
import UploadIcon from "../../assets/icons/UploadIcon";
import UploadIcon from "@/assets/icons/UploadIcon";
import CustomizedSwitch from "@ui_kit/CustomSwitch";
import {

@ -1,5 +1,5 @@
import { useState } from "react";
import UploadIcon from "@icons/UploadIcon";
import DeleteIcon from "@mui/icons-material/Delete";
import {
Box,
ButtonBase,
@ -10,11 +10,13 @@ import {
useTheme,
} from "@mui/material";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
import { enqueueSnackbar } from "notistack";
import { useState } from "react";
import { UploadImageModal } from "../../pages/Questions/UploadImage/UploadImageModal";
import { useDisclosure } from "../../utils/useDisclosure";
import imge from "@/assets/card-1.png"
import { CropModalInit } from "@/ui_kit/Modal/CropModal";
import DeleteIcon from "@mui/icons-material/Delete";
const allowedFileTypes = ["image/png", "image/jpeg", "image/gif"];
@ -25,7 +27,7 @@ interface Props {
imageUrl: string | null;
originalImageUrl: string | null;
onImageUploadClick: (image: Blob) => void;
onImageSaveClick: (image: Blob) => void;
onImageSavedClick?: () => void;
onDeleteClick: () => void;
cropAspectRatio?: {
width: number;
@ -41,22 +43,15 @@ export const DropZone = ({
imageUrl,
originalImageUrl,
onImageUploadClick,
onImageSaveClick,
onImageSavedClick,
onDeleteClick,
cropAspectRatio,
}: Props) => {
const theme = useTheme();
const quiz = useCurrentQuiz();
const [isDropReady, setIsDropReady] = useState<boolean>(false);
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] =
useDisclosure();
const {
isCropModalOpen,
openCropModal,
closeCropModal,
imageBlob,
setCropModalImageBlob,
} = useCropModalState();
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
if (!quiz) return null;
@ -68,7 +63,6 @@ export const DropZone = ({
onImageUploadClick?.(file);
closeImageUploadModal();
openCropModal(file);
}
const onDrop = (event: React.DragEvent<HTMLDivElement>) => {
@ -102,18 +96,8 @@ export const DropZone = ({
onClose={closeImageUploadModal}
handleImageChange={handleImageUpload}
/>
<CropModal
isOpen={isCropModalOpen}
imageBlob={imageBlob}
originalImageUrl={originalImageUrl}
setCropModalImageBlob={setCropModalImageBlob}
onClose={closeCropModal}
onSaveImageClick={onImageSaveClick}
cropAspectRatio={cropAspectRatio}
/>
<ButtonBase
onClick={
imageUrl ? () => openCropModal(imageUrl) : openImageUploadModal
onClick={ () => onImageSavedClick &&imageUrl ? onImageSavedClick() : openImageUploadModal()
}
sx={{
width: "100%",
@ -152,24 +136,25 @@ export const DropZone = ({
</Typography>
</>
)}
</ButtonBase>
{imageUrl && (
<IconButton
onClick={onDeleteClick}
sx={{
position: "absolute",
right: 0,
top: 0,
color: theme.palette.orange.main,
borderRadius: "8px",
borderBottomRightRadius: 0,
borderTopLeftRadius: 0,
...deleteIconSx,
}}
>
<DeleteIcon />
</IconButton>
)}
{imageUrl && (
<IconButton
onClick={onDeleteClick}
sx={{
position: "absolute",
right: 0,
top: 0,
color: theme.palette.orange.main,
borderRadius: "8px",
borderBottomRightRadius: 0,
borderTopLeftRadius: 0,
...deleteIconSx,
}}
>
<DeleteIcon />
</IconButton>
)}
</Box>
);
};

@ -64,7 +64,8 @@ export default function Extra() {
Дополнительно
</Link>
</Box>
{isExpanded && quiz && (
{/* {isExpanded && quiz && (
<Box
sx={{
backgroundColor: "transparent",
@ -89,7 +90,7 @@ export default function Extra() {
onChange={mutationOrgMetaHC}
/>
</Box>
)}
)} */}
</Box>
</Box>
);

@ -1,13 +1,13 @@
import { Box, Button, useMediaQuery, useTheme } from "@mui/material";
import CreationCard from "@ui_kit/CreationCard";
import quizCreationImage1 from "../../assets/quiz-creation-1.png";
import quizCreationImage2 from "../../assets/quiz-creation-2.png";
import quizCreationImage1 from "@/assets/quiz-creation-1.png";
import quizCreationImage2 from "@/assets/quiz-creation-2.png";
import { setQuizType, updateQuiz } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { useEffect, useRef, useState } from "react";
import arrowLeftIcon from "../../assets/icons/arrow_left.svg";
import arrowRightIcon from "../../assets/icons/arrow_right.svg";
import arrowLeftIcon from "@/assets/icons/arrow_left.svg";
import arrowRightIcon from "@/assets/icons/arrow_right.svg";
import QuizgenegationName from "@utils/quizgenegationName";
import { QuizType } from "@model/quizSettings";

@ -7,14 +7,14 @@ import {
} from "@mui/material";
import { setQuizStartpageType } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import cardImage1 from "../../assets/card-1.png";
import cardImage2 from "../../assets/card-2.png";
import cardImage3 from "../../assets/card-3.png";
import cardImage1 from "@/assets/card-1.png";
import cardImage2 from "@/assets/card-2.png";
import cardImage3 from "@/assets/card-3.png";
import CardWithImage from "./CardWithImage";
import { useRef, useState, useEffect } from "react";
import arrowLeftIcon from "../../assets/icons/arrow_left.svg";
import arrowRightIcon from "../../assets/icons/arrow_right.svg";
import arrowLeftIcon from "@/assets/icons/arrow_left.svg";
import arrowRightIcon from "@/assets/icons/arrow_right.svg";
export default function Steptwo() {
const quiz = useCurrentQuiz();

@ -4,11 +4,12 @@ import { devlog } from "@frontend/kitui";
import { AnyTypedQuizQuestion, QuestionVariant } from "@frontend/squzanswerer";
import { questionToEditQuestionRequest } from "@model/question/edit";
import { QuestionType, RawQuestion, rawQuestionToQuestion } from "@model/question/question";
import { UntypedQuizQuestion, createQuestionVariant } from "@model/questionTypes/shared";
import { UntypedQuizQuestion, createQuestionOwnVariant, createQuestionVariant } from "@model/questionTypes/shared";
import { produce } from "immer";
import { nanoid } from "nanoid";
import { enqueueSnackbar } from "notistack";
import { defaultQuestionByType } from "../../constants/default";
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
import { replaceEmptyLinesToSpace } from "../../utils/replaceEmptyLinesToSpace";
import { RequestQueue } from "../../utils/requestQueue";
import { QuestionsStore, useQuestionsStore } from "./store";
@ -336,6 +337,20 @@ export const addQuestionVariant = (questionId: string) => {
}
});
};
export const addQuestionOwnVariant = (questionId: string) => {
updateQuestion(questionId, (question) => {
switch (question.type) {
case "variant":
case "emoji":
case "images":
case "varimg":
question.content.variants.push(createQuestionOwnVariant());
break;
default:
throw new Error(`Cannot add variant to question of type "${question.type}"`);
}
});
};
export const deleteQuestionVariant = (questionId: string, variantId: string) => {
updateQuestion(questionId, (question) => {
@ -438,6 +453,7 @@ export const changeQuestionType = (questionId: string, type: QuestionType) => {
question.content = JSON.parse(JSON.stringify(defaultQuestionByType[type].content));
question.content.id = oldId;
question.content.rule = oldRule;
if ("editedUrlImagesList" in question.content) question.content.editedUrlImagesList = null;
});
};

@ -1,6 +1,6 @@
import { Box, Button, Skeleton, useTheme, useMediaQuery } from "@mui/material";
import Plus from "@icons/questionsPage/plus";
import Image from "../assets/icons/questionsPage/image";
import Image from "@/assets/icons/questionsPage/image";
import type { SxProps, Theme } from "@mui/material";

@ -15,7 +15,7 @@ import {
useContactFormStore,
} from "../stores/contactForm";
import { enqueueSnackbar } from "notistack";
import X from "../assets/icons/x";
import X from "@/assets/icons/x";
import PenaLogo from "../ui_kit/PenaLogo";
export default () => {

@ -9,6 +9,7 @@ import {
Typography,
useTheme,
} from "@mui/material";
import { enqueueSnackbar } from "notistack";
interface CustomTextFieldProps {
placeholder: string;
@ -71,6 +72,8 @@ export default function CustomTextField({
if (onChange) {
onChange(event);
}
} else {
enqueueSnackbar("Превышена длина вводимого текста")
}
};

@ -1,5 +1,6 @@
import EmojiPickerOriginal from "@emoji-mart/react";
import { Box } from "@mui/material";
import { I18n, Data } from "./emogiPickerUtils/MOCKdata"
type Emoji = {
emoticons: string[];
@ -22,6 +23,8 @@ export const EmojiPicker = ({ onEmojiSelect }: EmojiPickerProps) => (
theme="light"
locale="ru"
exceptEmojis={ignoreEmojis}
i18n={I18n}
data={Data}
/>
</Box>
);

@ -1,45 +1,61 @@
import InfoIcon from "@icons/InfoIcon";
import UploadIcon from "@icons/UploadIcon";
import { Box, Button, ButtonBase, Skeleton, Tooltip, Typography, useTheme } from "@mui/material";
import { updateQuestion, uploadQuestionImage } from "@root/questions/actions";
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
import UploadBox from "@ui_kit/UploadBox";
import { FC, useState } from "react";
import { UploadImageModal } from "../pages/Questions/UploadImage/UploadImageModal";
import { VideoElement } from "../pages/startPage/VideoElement";
import { useCurrentQuiz } from "../stores/quizes/hooks";
import { useDisclosure } from "../utils/useDisclosure";
import { QuizQuestionPage, QuizQuestionResult } from "@frontend/squzanswerer";
import UploadVideoModal from "@/pages/Questions/UploadVideoModal";
import {
Box,
Button,
ButtonBase,
Skeleton,
Tooltip,
Typography,
useTheme,
} from "@mui/material";
import { updateQuestion, uploadQuestionImage } from "@root/questions/actions";
interface Props {
question: QuizQuestionPage | QuizQuestionResult;
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
import { UploadImageModal } from "@/pages/Questions/UploadImage/UploadImageModal";
import { VideoElement } from "@/pages/startPage/VideoElement";
import { useCurrentQuiz } from "@/stores/quizes/hooks";
import { useDisclosure } from "@/utils/useDisclosure";
import UploadBox from "@ui_kit/UploadBox";
import UploadIcon from "@icons/UploadIcon";
import InfoIcon from "@icons/InfoIcon";
import imge from "@/assets/card-1.png"
import { CropModalInit } from "./Modal/CropModal";
import { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
interface Iprops {
question: AnyTypedQuizQuestion;
cropAspectRatio: {
width: number;
height: number;
};
}
export const MediaSelectionAndDisplay: FC<Props> = ({ question, cropAspectRatio }) => {
export const MediaSelectionAndDisplay: FC<Iprops> = ({
question,
cropAspectRatio,
}) => {
const [pictureUploding, setPictureUploading] = useState<boolean>(false);
const [backgroundUploding, setBackgroundUploading] = useState<boolean>(false);
const [openCropModal, setOpenCropModal] = useState(false);
const quizQid = useCurrentQuiz()?.qid;
const theme = useTheme();
const { isCropModalOpen, openCropModal, closeCropModal, imageBlob, originalImageUrl, setCropModalImageBlob } =
useCropModalState();
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
const [isVideoUploadDialogOpen, setIsVideoUploadDialogOpen] = useState<boolean>(false);
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] =
useDisclosure();
async function handleImageUpload(file: File) {
setPictureUploading(true);
const url = await uploadQuestionImage(question.id, quizQid, file, (question, url) => {
question.content.back = url;
question.content.originalBack = url;
});
closeImageUploadModal();
openCropModal(file, url);
const url = await uploadQuestionImage(
question.id,
quizQid,
file,
(question, url) => {
question.content.back = url;
question.content.originalBack = url;
},
);
setOpenCropModal(true)
setPictureUploading(false);
}
@ -95,11 +111,10 @@ export const MediaSelectionAndDisplay: FC<Props> = ({ question, cropAspectRatio
}}
variant="text"
onClick={() =>
updateQuestion(question.id, (question) => {
if (!("useImage" in question.content)) return;
question.content.useImage = true;
})
updateQuestion(
question.id,
(question) => (question.content.useImage = true),
)
}
>
Изображение
@ -124,32 +139,30 @@ export const MediaSelectionAndDisplay: FC<Props> = ({ question, cropAspectRatio
Видео
</Button>
</Box>
<UploadImageModal
isOpen={isImageUploadOpen}
onClose={closeImageUploadModal}
handleImageChange={handleImageUpload}
/>
<CropModal
isOpen={isCropModalOpen}
imageBlob={imageBlob}
originalImageUrl={originalImageUrl}
setCropModalImageBlob={setCropModalImageBlob}
onClose={closeCropModal}
onSaveImageClick={handleCropModalSaveClick}
onDeleteClick={() => {
updateQuestion(question.id, (question) => {
question.content.back = null;
question.content.originalBack = null;
});
<Box
sx={{
display: "flex",
flexDirection: "column",
}}
cropAspectRatio={cropAspectRatio}
/>
<UploadVideoModal
open={isVideoUploadDialogOpen}
onClose={() => setIsVideoUploadDialogOpen(false)}
onUpload={handleVideoUpload}
video={question.content.video}
/>
>
<UploadImageModal
isOpen={isImageUploadOpen}
onClose={closeImageUploadModal}
handleImageChange={handleImageUpload}
/>
<CropModalInit
originalImageUrl={question.content.originalBack}
editedUrlImagesList={question.content?.editedUrlImagesList}
questionId={question.id}
questionType={question.type}
quizId={quizQid}
open={openCropModal}
selfClose={() => setOpenCropModal(false)}
setPictureUploading={setPictureUploading}
/>
</Box>
{question.content.useImage && (
<Box
sx={{
@ -161,14 +174,14 @@ export const MediaSelectionAndDisplay: FC<Props> = ({ question, cropAspectRatio
}}
>
<AddOrEditImageButton
imageSrc={question.content.back ?? undefined}
imageSrc={question.content.back}
uploading={pictureUploding}
onImageClick={() => {
if (question.content.back) {
return openCropModal(question.content.back, question.content.originalBack);
setOpenCropModal(true)
} else {
openImageUploadModal();
}
openImageUploadModal();
}}
onPlusClick={() => {
openImageUploadModal();
@ -221,6 +234,27 @@ export const MediaSelectionAndDisplay: FC<Props> = ({ question, cropAspectRatio
my: "20px",
}}
>
<input
onChange={async (event) => {
setBackgroundUploading(true);
const file = event.target.files?.[0];
if (file) {
await uploadQuestionImage(
question.id,
quizQid,
file,
(question, url) => {
question.content.video = url;
},
);
}
setBackgroundUploading(false);
}}
hidden
accept=".mp4"
multiple
type="file"
/>
<UploadBox
icon={<UploadIcon />}
sx={{
@ -246,7 +280,8 @@ export const MediaSelectionAndDisplay: FC<Props> = ({ question, cropAspectRatio
/>
)}
</>
)}
</Box>
)
}
</Box >
);
};

@ -1,438 +0,0 @@
import { devlog } from "@frontend/kitui";
import { ResetIcon } from "@icons/ResetIcon";
import DeleteIcon from "@mui/icons-material/Delete";
import {
Box,
Button,
IconButton,
Modal,
Slider,
SxProps,
Theme,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { enqueueSnackbar } from "notistack";
import { FC, useCallback, useMemo, useRef, useState } from "react";
import ReactCrop, {
PercentCrop,
centerCrop,
convertToPixelCrop,
makeAspectCrop,
} from "react-image-crop";
import "react-image-crop/dist/ReactCrop.css";
import { isImageBlobAGifFile } from "../../utils/isImageBlobAGifFile";
import {
getModifiedImageBlob,
getRotatedImageBlob,
} from "./utils/imageManipulation";
const styleSlider: SxProps<Theme> = {
color: "#7E2AEA",
height: "12px",
"& .MuiSlider-track": {
border: "none",
},
"& .MuiSlider-rail": {
backgroundColor: "#F2F3F7",
border: `1px solid #9A9AAF`,
},
"& .MuiSlider-thumb": {
height: 26,
width: 26,
border: `6px solid #7E2AEA`,
backgroundColor: "white",
boxShadow: `0px 0px 0px 3px white,
0px 4px 4px 3px #C3C8DD`,
"&:focus, &:hover, &.Mui-active, &.Mui-focusVisible": {
boxShadow: `0px 0px 0px 3px white,
0px 4px 4px 3px #C3C8DD`,
},
},
};
interface Props {
isOpen: boolean;
imageBlob: Blob | null;
originalImageUrl: string | null;
setCropModalImageBlob: (imageBlob: Blob) => void;
onClose: () => void;
onSaveImageClick: (imageBlob: Blob) => void;
onDeleteClick?: () => void;
cropAspectRatio?: {
width: number;
height: number;
};
}
export const CropModal: FC<Props> = ({
isOpen,
imageBlob,
originalImageUrl,
setCropModalImageBlob,
onSaveImageClick,
onDeleteClick,
onClose,
cropAspectRatio,
}) => {
const theme = useTheme();
const [percentCrop, setPercentCrop] = useState<PercentCrop | undefined>(
undefined,
);
const [darken, setDarken] = useState(0);
const [imageWidth, setImageWidth] = useState<number | null>(null);
const [imageHeight, setImageHeight] = useState<number | null>(null);
const cropImageElementRef = useRef<HTMLImageElement>(null);
const isMobile = useMediaQuery(theme.breakpoints.down(786));
const imageUrl = useMemo(
() => imageBlob && URL.createObjectURL(imageBlob),
[imageBlob],
);
function resetEditState() {
setPercentCrop(undefined);
setDarken(0);
}
async function handleSaveModifiedImage() {
if (!percentCrop || !imageWidth || !imageHeight) return;
if (!cropImageElementRef.current) throw new Error("No image");
const width = cropImageElementRef.current.width;
const height = cropImageElementRef.current.height;
const pixelCrop = convertToPixelCrop(percentCrop, width, height);
try {
const blob = await getModifiedImageBlob(
cropImageElementRef.current,
pixelCrop,
darken,
);
onSaveImageClick?.(blob);
resetEditState();
onClose();
} catch (error) {
devlog("getCroppedImageBlob error", error);
enqueueSnackbar("Не удалось изменить изображение");
}
}
async function handleRotateClick() {
if (!cropImageElementRef.current) throw new Error("No image");
try {
const blob = await getRotatedImageBlob(cropImageElementRef.current);
setCropModalImageBlob(blob);
setPercentCrop(undefined);
} catch (error) {
devlog("getRotatedImageBlob error", error);
enqueueSnackbar("Не удалось изменить изображение");
}
}
async function handleSaveOriginalImage() {
if (!originalImageUrl) return;
const response = await fetch(originalImageUrl);
const blob = await response.blob();
onSaveImageClick?.(blob);
resetEditState();
onClose();
}
function handleSizeChange(value: number) {
setPercentCrop((prev) => {
if (!imageWidth || !imageHeight) return;
const crop = makeAspectCrop(
{
unit: "%",
width: value,
x: 0,
y: 0,
},
cropAspectRatio ? cropAspectRatio.width / cropAspectRatio.height : 1,
imageWidth,
imageHeight,
);
if (!prev || prev.height === 0 || prev.width === 0) {
return centerCrop(crop, imageWidth, imageHeight);
}
crop.x = Math.min(
100 - crop.width,
Math.max(0, prev.x + (prev.width - crop.width) / 2),
);
crop.y = Math.min(
100 - crop.height,
Math.max(0, prev.y + (prev.height - crop.height) / 2),
);
return crop;
});
}
return (
<Modal
open={isOpen}
onClose={() => {
resetEditState();
onClose();
}}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
bgcolor: "background.paper",
boxShadow: 24,
padding: "20px",
borderRadius: "8px",
width: isMobile ? "343px" : "620px",
height: isMobile ? "80vh" : undefined,
display: isMobile ? "flex" : undefined,
flexDirection: isMobile ? "column" : undefined,
justifyContent: isMobile ? "flex-start" : undefined,
overflow: isMobile ? "auto" : undefined,
}}
>
<Box
sx={{
height: "320px",
backgroundSize: "cover",
backgroundRepeat: "no-repeat",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{imageUrl && (
<ReactCrop
crop={percentCrop}
onChange={(_, percentCrop) => setPercentCrop(percentCrop)}
minWidth={5}
minHeight={5}
locked
aspect={
cropAspectRatio
? cropAspectRatio.width / cropAspectRatio.height
: undefined
}
>
<img
onLoad={(e) => {
setImageWidth(e.currentTarget.naturalWidth);
setImageHeight(e.currentTarget.naturalHeight);
if (cropImageElementRef.current) {
setPercentCrop(
getInitialCrop(
cropImageElementRef.current.width,
cropImageElementRef.current.height,
cropAspectRatio
? cropAspectRatio.width / cropAspectRatio.height
: 1,
),
);
}
}}
ref={cropImageElementRef}
alt="Crop me"
src={imageUrl}
style={{
filter: `brightness(${100 - darken}%)`,
maxWidth: "100%",
maxHeight: "320px",
display: "block",
objectFit: "contain",
}}
/>
</ReactCrop>
)}
</Box>
<Box
sx={{
mt: "40px",
display: isMobile ? "block" : "flex",
alignItems: "end",
justifyContent: "space-between",
}}
>
<IconButton onClick={handleRotateClick}>
<ResetIcon />
</IconButton>
<Box>
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
Размер
</Typography>
<Slider
sx={[
styleSlider,
{
width: isMobile ? undefined : "200px",
},
]}
value={percentCrop?.width ?? 1}
min={1}
max={100}
step={0.1}
onChange={(_, newValue) => {
if (typeof newValue === "number") handleSizeChange(newValue);
}}
/>
</Box>
<Box>
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
Затемнение
</Typography>
<Slider
sx={[
styleSlider,
{
width: isMobile ? undefined : "200px",
},
]}
value={darken}
min={0}
max={100}
step={1}
onChange={(_, newValue) => setDarken(newValue as number)}
/>
</Box>
{onDeleteClick && (
<IconButton
onClick={() => {
onDeleteClick?.();
onClose();
}}
sx={{
height: "48px",
width: "48px",
p: 0,
color: theme.palette.orange.main,
borderRadius: "50%",
}}
>
<DeleteIcon />
</IconButton>
)}
</Box>
<Box
sx={{
marginTop: "40px",
width: "100%",
display: "flex",
gap: "10px",
flexWrap: isMobile ? "wrap" : undefined,
}}
>
<Button
onClick={handleSaveOriginalImage}
disableRipple
sx={{
height: "48px",
color: "#7E2AEA",
borderRadius: "8px",
border: "1px solid #7E2AEA",
px: "20px",
}}
>
Сохранить оригинал
</Button>
<Button
onClick={handleSaveModifiedImage}
disableRipple
variant="contained"
sx={{
height: "48px",
borderRadius: "8px",
border: "1px solid #7E2AEA",
marginRight: "10px",
px: "20px",
ml: "auto",
}}
>
Сохранить редактированное
</Button>
</Box>
</Box>
</Modal>
);
};
export function useCropModalState(initialOpenState = false) {
const [isCropModalOpen, setOpened] = useState(initialOpenState);
const [imageBlob, setCropModalImageBlob] = useState<Blob | null>(null);
const [originalImageUrl, setOriginalImageUrl] = useState<string | null>(null);
const closeCropModal = useCallback(() => {
setOpened(false);
setCropModalImageBlob(null);
setOriginalImageUrl(null);
}, []);
const openCropModal = useCallback(
async (
image: Blob | string,
originalImageUrl: string | null | undefined = null,
) => {
if (typeof image === "string") {
const response = await fetch(image);
image = await response.blob();
}
const isGif = await isImageBlobAGifFile(image);
if (isGif) return;
setCropModalImageBlob(image);
setOriginalImageUrl(originalImageUrl);
setOpened(true);
},
[],
);
return {
isCropModalOpen,
openCropModal,
closeCropModal,
imageBlob,
setCropModalImageBlob,
originalImageUrl,
} as const;
}
function getInitialCrop(
imageWidth: number,
imageHeight: number,
aspectRatio: number,
): PercentCrop {
const imageAspectRatio = imageWidth / imageHeight;
return centerCrop(
{
width:
imageAspectRatio < aspectRatio
? 100
: (100 * aspectRatio) / imageAspectRatio,
height:
imageAspectRatio < aspectRatio
? (100 * imageAspectRatio) / aspectRatio
: 100,
unit: "%",
x: 0,
y: 0,
},
imageWidth,
imageHeight,
);
}

@ -0,0 +1,53 @@
import { Box, Button, Modal, Typography, useMediaQuery, useTheme } from "@mui/material";
interface Props {
open: boolean;
cancelDelete: () => void;
deleteImage: () => void;
}
export default ({
open,
cancelDelete,
deleteImage,
}: Props) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(450));
return (
<Modal
open={open}
onClose={cancelDelete}
>
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
bgcolor: "background.paper",
boxShadow: 24,
borderRadius: "12px",
width: isMobile ? "200px" : "350px",
height: "350px",
display: "flex",
flexDirection: "column",
}}
>
<Typography sx={{
// height: isMobile ? "91px" : "70px",
backgroundColor: "#F2F3F7",
padding: isMobile ? "25px 20px 24px 20px" : "25px 43px 24px 20px",
borderRadius: "8px 8px 0px 0px",
color: "#9A9AAF",
fontSize: "18px",
lineHeight: "21.33px"
}}>
Вы уверены, что хотите удалить всю картинку и каждую настройку?
</Typography>
<Button sx={{margin: "25px"}} variant="contained" onClick={cancelDelete}>нет</Button>
<Button sx={{margin: "25px"}} variant="outlined" onClick={deleteImage}>да</Button>
</Box>
</Modal>
);
};

@ -0,0 +1,328 @@
import { FC, useCallback, useEffect, useRef, useState } from "react";
import { enqueueSnackbar } from "notistack";
import ReactCrop, {
PercentCrop,
centerCrop,
convertToPixelCrop,
makeAspectCrop,
type Crop
} from "react-image-crop";
import {
getModifiedImageBlob,
getRotatedImageBlob,
} from "./utils/imageManipulation";
import {
Box,
IconButton,
Slider,
SxProps,
Theme,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { ResetIcon } from "@icons/ResetIcon";
import "react-image-crop/dist/ReactCrop.css";
import { EditedImagesChangeType } from "./CropModal";
import { CropAspectRatio, DEFAULTCROPRULES, EditedImage } from "@/model/CropModal/CropModal";
import { devlog } from "@frontend/kitui";
const styleSlider: SxProps<Theme> = {
color: "#7E2AEA",
height: "10px",
p: "18px 0",
"& .MuiSlider-track": {
border: "none",
},
"& .MuiSlider-rail": {
backgroundColor: "#F2F3F7",
border: `1px solid #9A9AAF`,
},
"& .MuiSlider-thumb": {
height: 24,
width: 24,
border: `6px solid #7E2AEA`,
backgroundColor: "white",
boxShadow: `0px 0px 0px 3px white,
0px 4px 4px 3px #C3C8DD`,
"&:focus, &:hover, &.Mui-active, &.Mui-focusVisible": {
boxShadow: `0px 0px 0px 3px white,
0px 4px 4px 3px #C3C8DD`,
},
},
};
interface Props {
editedImage: EditedImage;
cropAspectRatio: CropAspectRatio;
editedImagesChange: EditedImagesChangeType;
};
export const CropGeneral: FC<Props> = ({
editedImage,
cropAspectRatio,
editedImagesChange,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(786));
const { crop, darken } = editedImage.newRules;
const cropImageElementRef = useRef<HTMLImageElement>(null);
const [imageWidth, setImageWidth] = useState<number | null>(null);
const [imageHeight, setImageHeight] = useState<number | null>(null);
async function handleRotateClick() {
if (cropImageElementRef.current !== null) {
try {
const blob = await getRotatedImageBlob(cropImageElementRef.current);
editedImagesChange((old) => {
const newRotate = old.newRules.rotate + 90;
return {
newRules: {
...old.newRules,
rotate: newRotate > 360 ? 0 : newRotate
},
url: (URL.createObjectURL(blob))
};
});
} catch (error) {
devlog("getRotatedImageBlob error", error);
enqueueSnackbar("Не удалось изменить изображение");
}
}
}
function handleSizeChange(value: number) {
editedImagesChange((old) => {
if (!imageWidth || !imageHeight) return old;
const crop = makeAspectCrop(
{
unit: "%",
width: value,
x: 0,
y: 0,
},
cropAspectRatio ? cropAspectRatio.width / cropAspectRatio.height : 1,
imageWidth,
imageHeight,
);
//Хз зачем это было нужно, как будет работать - перетещу
// if (!old.newRules.crop || old.newRules.crop.height === 0 || old.newRules.crop.width === 0) {
// return centerCrop(crop, imageWidth, imageHeight);
// }
crop.x = Math.min(
100 - crop.width,
Math.max(0, old.newRules.crop.x + (old.newRules.crop.width - crop.width) / 2),
);
crop.y = Math.min(
100 - crop.height,
Math.max(0, old.newRules.crop.y + (old.newRules.crop.height - crop.height) / 2),
);
return {
newRules: {
...old.newRules,
crop
}
};
});
}
const calcCrop = () => {
if (cropImageElementRef.current) {
editedImagesChange((old) => ({
newRules: {
...old.newRules,
crop: getInitialCrop(
cropImageElementRef.current?.width,
cropImageElementRef.current?.height,
cropAspectRatio
? cropAspectRatio.width / cropAspectRatio.height
: 2,
)
}
}))
}
}
useEffect(() => {
if (!crop.width && !crop.height) { calcCrop() }
}, [crop])
return (
<>
<Box
sx={{
height: "320px",
backgroundSize: "cover",
backgroundRepeat: "no-repeat",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "0 20px",
marginTop: isMobile ? "19px" : "20px",
}}
>
<ReactCrop
crop={crop}
onChange={(_, percentCrop) => editedImagesChange((old) => {
return ({
newRules: {
...old.newRules,
crop: percentCrop
}
})
}
)}
minWidth={5}
minHeight={5}
locked
aspect={
cropAspectRatio
? cropAspectRatio.width / cropAspectRatio.height
: undefined
}
>
<img
id="imgid"
onLoad={(e) => {
setImageWidth(e.currentTarget.naturalWidth);
setImageHeight(e.currentTarget.naturalHeight);
calcCrop()
}}
ref={cropImageElementRef}
alt="Crop me"
src={editedImage.url}
style={{
filter: `brightness(${100 - editedImage.newRules.darken}%)`,
maxWidth: "100%",
height: "320px",
maxHeight: "320px",
display: "block",
objectFit: "contain",
}}
crossOrigin = 'anonymous'
/>
</ReactCrop>
</Box>
<Box
sx={{
mt: isMobile ? "20px" : "48px",
display: "flex",
alignItems: "end",
justifyContent: "space-between",
padding: "0 20px",
flexDirection: isMobile ? "column" : "",
}}
>
<IconButton onClick={handleRotateClick}
sx={{
mb: "11px",
p: "0",
}}
>
<ResetIcon />
</IconButton>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
flexDirection: isMobile ? "column" : "",
width: isMobile ? "100%" : "auto",
gap: "24px",
}}
>
<Box>
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
Размер
</Typography>
<Slider
sx={[
styleSlider,
{
width: isMobile ? undefined : "248px",
},
]}
value={crop.width}
min={1}
max={100}
step={0.1}
onChange={(_, newValue) => {
if (typeof newValue === "number") handleSizeChange(newValue);
}}
/>
</Box>
<Box>
<Typography sx={{ color: "#9A9AAF", fontSize: "16px", ml: "-1px", }}>
Затемнение
</Typography>
<Slider
sx={[
styleSlider,
{
width: isMobile ? undefined : "248px",
},
]}
value={darken}
min={0}
max={100}
step={1}
onChange={(_, newValue) => editedImagesChange((old) => ({
newRules: {
...old.newRules,
darken: newValue as number
}
}))}
/>
</Box>
</Box>
</Box>
</>
)
};
function getInitialCrop(
imageWidth: number | null | undefined,
imageHeight: number | null | undefined,
aspectRatio: number,
): PercentCrop {
if (!imageHeight || !imageWidth) return DEFAULTCROPRULES.crop
const imageAspectRatio = imageWidth / imageHeight;
return centerCrop(
{
width:
imageAspectRatio < aspectRatio
? 100
: (100 * aspectRatio) / imageAspectRatio,
height:
imageAspectRatio < aspectRatio
? (100 * imageAspectRatio) / aspectRatio
: 100,
unit: "%",
x: 0,
y: 0,
},
imageWidth,
imageHeight,
);
}

@ -0,0 +1,122 @@
import { FC, useEffect, useMemo, useRef, useState } from "react";
import WorkSpace from "./WorkSpace";
import { NavigationPanel } from "./NavigationPanel";
import {
Box,
Modal,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import {
DEFAULTCROPRULES,
type CropModalProps,
type EditedImage,
type ScreenStepsTypes
} from "@/model/CropModal/CropModal";
const PriorityOfSteps = ["desktop", "tablet", "mobile", "small"];
export type EditedImagesChangeType = (changed: (old: EditedImage) => Partial<EditedImage>) => void;
export const CropModal: FC<CropModalProps> = ({
open,
editedImages,
workSpaceTypes,
originalImageUrl,
setEditedImages,
onSaveImageClick,
closeCropModal,
onDeleteClick,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(786));
const [currentStep, setCurrentStep] = useState<number>(0);
const currentStepName: ScreenStepsTypes = useMemo(() => {
const mainSteps = Object.keys(workSpaceTypes);
const POS = PriorityOfSteps.filter(POS => mainSteps.find(e => POS === e))
return POS[currentStep] as ScreenStepsTypes
}, [currentStep])
const nextStepName: ScreenStepsTypes = useMemo(() => {
const mainSteps = Object.keys(workSpaceTypes);
const POS = PriorityOfSteps.filter(POS => mainSteps.find(e => POS === e))
return POS[currentStep+1] as ScreenStepsTypes
}, [currentStep])
const editedImagesChange: EditedImagesChangeType = (changed) => {
setEditedImages(old => {
const newData = { ...old };
newData[currentStepName] = { ...old[currentStepName], ...changed(old[currentStepName]) };
return newData;
})
}
const resetImage = () => {
editedImagesChange(() => ({
url: originalImageUrl,
newRules: DEFAULTCROPRULES,
}))
}
return (
<Modal
open={open}
onClose={closeCropModal}
>
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
bgcolor: "background.paper",
boxShadow: 24,
borderRadius: "12px",
width: isMobile ? "343px" : "620px",
height: isMobile ? "80vh" : undefined,
display: isMobile ? "flex" : undefined,
flexDirection: isMobile ? "column" : undefined,
justifyContent: isMobile ? "flex-start" : undefined,
overflow: isMobile ? "auto" : undefined,
}}
>
<Typography sx={{
// height: isMobile ? "91px" : "70px",
backgroundColor: "#F2F3F7",
padding: isMobile ? "25px 20px 24px 20px" : "25px 43px 24px 20px",
borderRadius: "8px 8px 0px 0px",
color: "#9A9AAF",
fontSize: "18px",
lineHeight: "21.33px"
}}>
Настройте вариант отображения картинки на разных девайсах
</Typography>
<WorkSpace
//Информация о изменяемой сейчас картинке
editedImage={editedImages[currentStepName]}
//По каким правилам меняем
cropAspectRatio={workSpaceTypes[currentStepName].ratio}
currentStep={currentStep}
currentStepName={currentStepName}
editedImagesChange={editedImagesChange}
onDeleteClick={onDeleteClick}
/>
<NavigationPanel
nextStepName={nextStepName}
currentStep={currentStep}
setCurrentStep={setCurrentStep}
totalSteps={Object.keys(workSpaceTypes).length}
onSaveImageClick={onSaveImageClick}
resetImage={resetImage}
/>
</Box>
</Modal>
);
};

@ -0,0 +1,24 @@
import {Box} from "@mui/material";
export default function DevaceDesktopIcon() {
return (
<Box
sx={{
height: "36px",
width: "36px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "6px",
backgroundColor: "#EEE4FC"
}}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="17" width="20" height="3" rx="1" stroke="#7E2AEA" strokeWidth="1.5"/>
<rect x="3" y="5" width="18" height="12" rx="1" stroke="#7E2AEA" strokeWidth="1.5"/>
<path d="M14 5.5L10 5.5" stroke="#7E2AEA" strokeWidth="2.5" strokeLinecap="round"/>
</svg>
</Box>
);
}

@ -0,0 +1,23 @@
import {Box} from "@mui/material";
export default function DevaceMobileIcon() {
return (
<Box
sx={{
height: "36px",
width: "36px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "6px",
backgroundColor: "#EEE4FC"
}}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="6" y="2" width="12" height="20" rx="3" stroke="#7E2AEA" strokeWidth="1.5"/>
<path d="M14 2.5L10 2.5" stroke="#7E2AEA" strokeWidth="2.5" strokeLinecap="round"/>
</svg>
</Box>
);
}

Some files were not shown because too many files have changed in this diff Show More