xMerge branch 'staging' into 'main'

refactored YandexModal, added yandexMetricNumber to quiz config, added logic...

See merge request frontend/squiz!282
This commit is contained in:
Nastya 2024-04-18 19:24:43 +00:00 committed by skeris
commit ced2e8cc80
66 changed files with 3036 additions and 122 deletions

@ -39,27 +39,23 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;600&display=swap" rel="stylesheet" />
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function (m, e, t, r, i, k, a) {
m[i] = m[i] || function () { (m[i].a = m[i].a || []).push(arguments) };
m[i].l = 1 * new Date();
for (var j = 0; j < document.scripts.length; j++) { if (document.scripts[j].src === r) { return; } }
k = e.createElement(t), a = e.getElementsByTagName(t)[0], k.async = 1, k.src = r, a.parentNode.insertBefore(k, a)
})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
<!-- Yandex.Metrika counter -->
<script type="text/javascript" >
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
ym(96979625, "init", {
clickmap: true,
trackLinks: true,
accurateTrackBounce: true,
webvisor: true
});
</script>
<noscript>
<div><img src="https://mc.yandex.ru/watch/96979625" style="position:absolute; left:-9999px;" alt="" /></div>
</noscript>
<!-- /Yandex.Metrika counter -->
ym(96979576, "init", {
clickmap:true,
trackLinks:true,
accurateTrackBounce:true,
webvisor:true
});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/96979576" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</head>
<body>
@ -67,4 +63,4 @@
<div id="root"></div>
</body>
</html>
</html>

@ -46,11 +46,15 @@ import OutdatedLink from "./pages/auth/OutdatedLink";
import { useAfterpay } from "@utils/hooks/useAfterpay";
const MyQuizzesFull = lazy(() => import("./pages/createQuize/MyQuizzesFull"));
const ViewPage = lazy(() => import("./pages/ViewPublicationPage"));
const Analytics = lazy(() => import("./pages/Analytics/Analytics"));
const EditPage = lazy(() => import("./pages/startPage/EditPage"));
const { Tariffs } = lazily(() => import("./pages/Tariffs/Tariffs"));
const { DesignPage } = lazily(() => import("./pages/DesignPage/DesignPage"));
const { IntegrationsPage } = lazily(
() => import("./pages/IntegrationsPage/IntegrationsPage"),
);
const { QuizAnswersPage } = lazily(
() => import("./pages/QuizAnswersPage/QuizAnswersPage"),
);
@ -75,6 +79,13 @@ const routeslink = [
sidebar: true,
footer: true,
},
{
path: "/integrations",
page: IntegrationsPage,
header: true,
sidebar: true,
footer: true,
},
] as const;
const LazyLoading = ({ children, fallback }: SuspenseProps) => (

@ -6,26 +6,38 @@ import { clearUserData } from "@root/user";
import { clearQuizData } from "@root/quizes/store";
import { redirect } from "react-router-dom";
interface MakeRequest {
method?: Method | undefined;
url: string;
body?: unknown;
useToken?: boolean | undefined;
contentType?: boolean | undefined;
responseType?: ResponseType | undefined;
signal?: AbortSignal | undefined;
withCredentials?: boolean | undefined;
}
interface MakeRequest { method?: Method | undefined; url: string; body?: unknown; useToken?: boolean | undefined; contentType?: boolean | undefined; responseType?: ResponseType | undefined; signal?: AbortSignal | undefined; withCredentials?: boolean | undefined; }
async function makeRequest<TRequest = unknown, TResponse = unknown>(
data: MakeRequest,
): Promise<TResponse> {
try {
const response = await KIT.makeRequest<unknown>(data);
async function makeRequest<TRequest = unknown, TResponse = unknown>(data: MakeRequest): Promise<TResponse> {
try {
const response = await KIT.makeRequest<unknown>(data)
return response as TResponse
} catch (e) {
const error = e as AxiosError;
//@ts-ignore
if (error.response?.status === 400 && error.response?.data?.message === "refreshToken is empty") {
cleanAuthTicketData();
clearAuthToken();
clearUserData();
clearQuizData();
redirect("/");
}
throw e
};
};
export default makeRequest;
return response as TResponse;
} catch (e) {
const error = e as AxiosError;
//@ts-ignore
if (
error.response?.status === 400 &&
error.response?.data?.message === "refreshToken is empty"
) {
cleanAuthTicketData();
clearAuthToken();
clearUserData();
clearQuizData();
redirect("/");
}
throw e;
}
}
export default makeRequest;

File diff suppressed because one or more lines are too long

@ -3,11 +3,13 @@ import { Box, useTheme } from "@mui/material";
interface CheckboxIconProps {
checked?: boolean;
color?: string;
isRounded?: boolean;
}
export default function CheckboxIcon({
checked = false,
color = "#7E2AEA",
isRounded,
}: CheckboxIconProps) {
const theme = useTheme();
@ -16,7 +18,7 @@ export default function CheckboxIcon({
sx={{
height: "24px",
width: "24px",
borderRadius: "6px",
borderRadius: isRounded ? "50%" : "6px",
display: "flex",
justifyContent: "center",
alignItems: "center",

@ -0,0 +1,36 @@
import { Box } from "@mui/material";
interface Props {
color: string;
height: string;
width: string;
}
export default function EditPencil({ color, height, width }: Props) {
return (
<Box
sx={{
height,
width,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<svg
width="19"
height="18"
viewBox="0 0 19 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M10.5137 0.80552C11.5869 -0.269234 13.3274 -0.269892 14.4013 0.804049L16.8932 3.2959C17.958 4.36068 17.969 6.08444 16.918 7.16281L7.68486 16.6361C6.97933 17.3599 6.01167 17.7681 5.00124 17.7681L2.24909 17.768C0.969844 17.7679 -0.0517699 16.7015 0.00203171 15.4224L0.120186 12.6134C0.159684 11.6744 0.549963 10.7844 1.2138 10.1195L10.5137 0.80552ZM13.3415 1.86551C12.8533 1.37736 12.0622 1.37766 11.5744 1.86618L9.9113 3.53178L14.1911 7.81157L15.8446 6.11505C16.3224 5.62488 16.3173 4.84136 15.8333 4.35737L13.3415 1.86551ZM2.27446 11.1802L8.85145 4.59325L13.144 8.88585L6.61148 15.5883C6.18816 16.0226 5.60756 16.2675 5.0013 16.2675L2.24916 16.2674C1.82274 16.2674 1.4822 15.9119 1.50014 15.4856L1.61829 12.6765C1.64199 12.1131 1.87616 11.5791 2.27446 11.1802ZM17.5148 17.6948C17.9289 17.6948 18.2645 17.3589 18.2645 16.9445C18.2645 16.5301 17.9289 16.1942 17.5148 16.1942H11.3931C10.9791 16.1942 10.6434 16.5301 10.6434 16.9445C10.6434 17.3589 10.9791 17.6948 11.3931 17.6948H17.5148Z"
fill="#7E2AEA"
/>
</svg>
</Box>
);
}

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Слой_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 36 36" style="enable-background:new 0 0 36 36;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:none;stroke:#7E2AEA;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;}
</style>
<g>
<path class="st0" d="M18,36L18,36C8.1,36,0,27.9,0,18l0,0C0,8.1,8.1,0,18,0l0,0c9.9,0,18,8.1,18,18l0,0C36,27.9,27.9,36,18,36z"/>
</g>
<path class="st1" d="M10.9,15.2L18,23l7-7.8"/>
</svg>

After

Width:  |  Height:  |  Size: 686 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

@ -0,0 +1,81 @@
import * as React from "react";
import { FC } from "react";
import Radio from "@mui/material/Radio";
import RadioGroup from "@mui/material/RadioGroup";
import FormControlLabel from "@mui/material/FormControlLabel";
import Box from "@mui/material/Box";
import CheckboxIcon from "@icons/Checkbox";
import { useTheme } from "@mui/material";
type CustomRadioGroupProps = {
items: string[];
selectedValue: string | null;
setSelectedValue: (value: string | null) => void;
};
export const CustomRadioGroup: FC<CustomRadioGroupProps> = ({
items,
selectedValue,
setSelectedValue,
}) => {
const theme = useTheme();
const [currentValue, setCurrentValue] = React.useState<string | null>(
selectedValue,
);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSelectedValue((event.target as HTMLInputElement).value);
setCurrentValue((event.target as HTMLInputElement).value);
};
return (
<Box
sx={{
border: `1px solid ${theme.palette.grey2.main}`,
borderRadius: "12px",
padding: "5px",
height: "100%",
overflowY: "auto",
}}
>
<RadioGroup
aria-labelledby="demo-controlled-radio-buttons-group"
name="controlled-radio-buttons-group"
value={currentValue}
onChange={handleChange}
>
{items.map((item) => (
<FormControlLabel
key={item}
sx={{
color: "black",
padding: "15px",
borderBottom: `1px solid ${theme.palette.background.default}`,
display: "flex",
justifyContent: "space-between",
borderRadius: "12px",
margin: 0,
backgroundColor:
currentValue === item
? theme.palette.background.default
: theme.palette.common.white,
}}
value={item}
control={
<Radio
checkedIcon={
<CheckboxIcon
checked
isRounded
color={theme.palette.brightPurple.main}
/>
}
icon={<CheckboxIcon isRounded />}
/>
}
label={item}
labelPlacement={"start"}
/>
))}
</RadioGroup>
</Box>
);
};

@ -0,0 +1,22 @@
.MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline {
border: 0;
}
.MuiPaper-root.MuiMenu-paper {
padding-top: 50px;
margin-top: -50px;
border-radius: 28px;
}
.MuiInputBase-root.MuiOutlinedInput-root {
display: block;
}
.MuiInputBase-root.MuiOutlinedInput-root > div:first-child,
.MuiInputBase-root.MuiOutlinedInput-root .MuiSelect-icon {
display: none;
}
.MuiMenu-root.MuiModal-root {
z-index: 0;
}

@ -0,0 +1,129 @@
import {
Avatar,
MenuItem,
Select,
SelectChangeEvent,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import Box from "@mui/material/Box";
import { FC, useCallback, useRef, useState } from "react";
import "./CustomSelect.css";
import arrow_down from "../../assets/icons/arrow_down.svg";
type CustomSelectProps = {
items: string[];
selectedItem: string | null;
setSelectedItem: (value: string | null) => void;
};
export const CustomSelect: FC<CustomSelectProps> = ({
items,
selectedItem,
setSelectedItem,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const [opened, setOpened] = useState<boolean>(false);
const [currentValue, setCurrentValue] = useState<string | null>(selectedItem);
const ref = useRef<HTMLDivElement | null>(null);
const onSelectItem = useCallback(
(event: SelectChangeEvent<HTMLDivElement>) => {
const newValue = event.target.value.toString();
setCurrentValue(newValue);
setSelectedItem(newValue);
},
[setSelectedItem, setCurrentValue],
);
const toggleOpened = useCallback(() => {
setOpened((isOpened) => !isOpened);
}, []);
return (
<Box>
<Box
sx={{
zIndex: 3,
position: "relative",
width: "100%",
height: "56px",
padding: "5px",
color:
currentValue === null
? theme.palette.grey2.main
: theme.palette.brightPurple.main,
border: `2px solid ${theme.palette.common.white}`,
borderRadius: "30px",
background: "#EFF0F5",
display: "flex",
alignItems: "center",
cursor: "pointer",
}}
onClick={() => ref.current?.click()}
>
<Avatar sx={{ width: 46, height: 46, marginRight: 1 }} />
<Typography
sx={{
marginLeft: isMobile ? "10px" : "20px",
fontWeight: currentValue === null ? "400" : "500",
fontSize: isMobile ? "14px" : "16px",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden",
flexGrow: 1,
}}
>
{currentValue || "Выберите ответственного за сделку"}
</Typography>
<img
src={arrow_down}
alt="check"
style={{
position: "absolute",
top: "50%",
right: "10px",
transform: `translateY(-50%) rotate(${opened ? "180deg" : "0deg"}`,
height: "36px",
width: "36px",
}}
className={`select-icon ${opened ? "opened" : ""}`}
/>
</Box>
<Select
ref={ref}
className="select"
value={""}
open={opened}
MenuProps={{
disablePortal: true,
PaperProps: {
style: {
zIndex: 2,
maxHeight: "300px",
overflow: "auto",
},
},
}}
sx={{ width: "100%" }}
onChange={onSelectItem}
onClick={toggleOpened}
>
{items.map((item) => {
const uniqueKey = `${item}-${Date.now()}`;
return (
<MenuItem
key={uniqueKey}
value={item}
sx={{ padding: "12px", zIndex: 2 }}
>
{item}
</MenuItem>
);
})}
</Select>
</Box>
);
};

@ -2,9 +2,7 @@ import ChartPieIcon from "@icons/ChartPieIcon";
import ContactBookIcon from "@icons/ContactBookIcon";
import FlowArrowIcon from "@icons/FlowArrowIcon";
import LayoutIcon from "@icons/LayoutIcon";
import MegaphoneIcon from "@icons/MegaphoneIcon";
import QuestionIcon from "@icons/QuestionIcon";
import QuestionsMapIcon from "@icons/QuestionsMapIcon";
export const quizSetupSteps = [
{
@ -118,6 +116,7 @@ export interface QuizConfig {
law?: string;
};
meta: string;
yandexMetricNumber: number | undefined;
}
export type FormContactFieldName =
@ -225,4 +224,5 @@ export const defaultQuizConfig: QuizConfig = {
button: "",
},
meta: "",
yandexMetricNumber: undefined,
};

@ -154,6 +154,7 @@ export default function Analytics() {
onClose={handleClose}
onOpen={handleOpen}
// defaultValue={now}
minDate={moment(quiz?.created_at)}
sx={{
width: isMobile ? "285px" : "170px",
"& .MuiOutlinedInput-root": {
@ -199,6 +200,7 @@ export default function Analytics() {
onClose={handleCloseEnd}
onOpen={handleOpenEnd}
// defaultValue={now}
minDate={moment(quiz?.created_at)}
sx={{
width: isMobile ? "285px" : "170px",
"& .MuiOutlinedInput-root": {

@ -112,30 +112,48 @@ const GeneralItemTimeConv = ({
numberType,
calculateTime = false,
conversionValue,
day,
}: GeneralItemsProps) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(700));
const data = Object.entries(general).sort(
([nextValue], [currentValue]) => Number(nextValue) - Number(currentValue),
);
const days = data.map(([value]) => value);
const time = data.map(([_, value]) => value);
const data = Object.entries(general)
.sort((a, b) => a[0] - b[0]);
const numberValue = calculateTime
? time.reduce((total, value) => total + value, 0) / days.length
: conversionValue;
const days = [...data].map(e => e[0])
let buffer = 0
const time = [...data].map(e => {
if (e[1] > 0) {
buffer = e[1]
}
return buffer
})
console.log("data", data)
console.log("time", time.reduce((a, b) => (Number(a) + Number(b)), 0))
console.log("time", getCalculatedTime(time.reduce((a, b) => (Number(a) + Number(b)), 0)))
console.log("days", days.length)
const numberValue = calculateTime ?
(
(time.reduce((a, b) => (Number(a) + Number(b)), 0))
/
(days.length)
) || 0
:
conversionValue
if (
Object.keys(general).length === 0 ||
Object.values(general).every((item) => item === 0)
Object.values(general).every((x) => x === 0)
) {
return (
<Typography textAlign="center">{`${title} - нет данных`}</Typography>
);
}
return (
<Paper
sx={{
@ -146,25 +164,24 @@ const GeneralItemTimeConv = ({
>
<Typography sx={{ margin: "20px 20px 0" }}>{title}</Typography>
<Typography sx={{ margin: "10px 20px 0", fontWeight: "bold" }}>
{calculateTime
? `${getCalculatedTime(numberValue ?? 0)} с`
: `${numberValue?.toFixed(2) ?? 0}%`}
{calculateTime ? `${getCalculatedTime(numberValue)} с` : `${numberValue.toFixed(2)}%`}
</Typography>
<LineChart
xAxis={[
{
data: days,
valueFormatter: (value) =>
moment.unix(Number(value)).format("DD/MM/YYYY HH") + "ч",
moment.utc(Number(value) * 1000).format("DD/MM/YYYY"),
},
]}
series={[
{
data: Object.values(time),
valueFormatter: (value) =>
calculateTime
? getCalculatedTime(value)
: String((value * 100).toFixed(2)) + "%",
valueFormatter: (value) => {
console.log("log", value)
return calculateTime ? getCalculatedTime(value) : String((value*100).toFixed(2)) + "%"
}
,
},
]}
// dataset={Object.entries(general).map(([, v]) => moment.unix(v).format("ss:mm:HH")).reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {})}
@ -179,6 +196,7 @@ const GeneralItemTimeConv = ({
);
};
export const General: FC<GeneralProps> = ({ data, day }) => {
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(1000));

@ -8,20 +8,19 @@ import {
} from "@mui/material";
import { updateQuiz } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import type { DesignItem } from "./DesignGroup";
import { DesignGroup } from "./DesignGroup";
import Desgin1 from "@icons/designs/design1.jpg";
import Desgin2 from "@icons/designs/design2.jpg";
import Desgin3 from "@icons/designs/design3.jpg";
import Desgin4 from "@icons/designs/design4.jpg";
import Desgin5 from "@icons/designs/design5.jpg";
import Desgin6 from "@icons/designs/design6.jpg";
import Desgin7 from "@icons/designs/design7.jpg";
import Desgin8 from "@icons/designs/design8.jpg";
import Desgin9 from "@icons/designs/design9.jpg";
import Desgin10 from "@icons/designs/design10.jpg";
import type { DesignItem } from "./DesignGroup";
import Desgin1 from "@icons/designs/smallSize/design1.jpg";
import Desgin2 from "@icons/designs/smallSize/design2.jpg";
import Desgin3 from "@icons/designs/smallSize/design3.jpg";
import Desgin4 from "@icons/designs/smallSize/design4.jpg";
import Desgin5 from "@icons/designs/smallSize/design5.jpg";
import Desgin6 from "@icons/designs/smallSize/design6.jpg";
import Desgin7 from "@icons/designs/smallSize/design7.jpg";
import Desgin8 from "@icons/designs/smallSize/design8.jpg";
import Desgin9 from "@icons/designs/smallSize/design9.jpg";
import Desgin10 from "@icons/designs/smallSize/design10.jpg";
const LIGHT_THEME_BUTTONS: DesignItem[] = [
{

@ -76,8 +76,10 @@ export default function InstallQuiz() {
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1065));
const CopyLink = () => {
let one = document.getElementById("inputLinkone").value;
let text = document.getElementById("inputLink").value;
let one = (document.getElementById("inputLinkone") as HTMLInputElement)
?.value;
let text = (document.getElementById("inputLink") as HTMLInputElement)
?.value;
// text.select();
navigator.clipboard.writeText(one + text);
// document.execCommand("copy");
@ -408,7 +410,7 @@ export default function InstallQuiz() {
id="outlined-multiline-static"
multiline
rows={9}
value={`<div id="idpena"></div> <script type="module"> import widget from "https://s.hbpn.link/export/pub.js"; widget.create({ selector: "idpena", quizId: ${quiz.qid} }) </script>`}
value={`<div id="idpena"></div> <script type="module"> import widget from "https://${isTestServer ? "s." : ""}hbpn.link/export/pub.js"; widget.create({ selector: "idpena", quizId: ${quiz.qid} }) </script>`}
sx={{
"& .MuiInputBase-root": {
maxWidth: "520px",

@ -0,0 +1,41 @@
import { Box, Typography, useTheme } from "@mui/material";
import { FC } from "react";
import YandexMetric from "../mocks/YandexMetric.png";
type PartnerItemProps = {
setIsModalOpen: (value: boolean) => void;
setCompanyName?: (value: string) => void;
};
export const YandexButton: FC<PartnerItemProps> = ({
setIsModalOpen,
setCompanyName,
}) => {
const theme = useTheme();
const handleClick = () => {
setIsModalOpen(true);
};
return (
<>
<Box
sx={{
width: 250,
height: 60,
backgroundColor: "white",
borderRadius: "8px",
padding: "0 20px",
display: "flex",
alignItems: "center",
marginBottom: "2%",
marginRight: "2%",
cursor: "pointer",
}}
onClick={() => setIsModalOpen(true)}
>
<img width={"100%"} src={YandexMetric} alt={"Yandex.Метрика"} />
</Box>
</>
);
};

@ -0,0 +1,186 @@
import {
Button,
Dialog,
IconButton,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import Box from "@mui/material/Box";
import CloseIcon from "@mui/icons-material/Close";
import React, { useState } from "react";
import CustomTextField from "@ui_kit/CustomTextField";
import EditPencil from "@icons/EditPencil";
import Trash from "@icons/trash";
import { updateQuiz } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
interface Props {
isModalOpen: boolean;
handleCloseModal: () => void;
}
export default function YandexModal({ isModalOpen, handleCloseModal }: Props) {
const theme = useTheme();
const quiz = useCurrentQuiz();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const yandexNumber = quiz?.config.yandexMetricNumber;
const [isSave, setIsSave] = useState<boolean>(!!yandexNumber);
const [currentValue, setCurrentValue] = useState<string>(
yandexNumber ? yandexNumber.toString() : "",
);
const handleSave = () => {
updateQuiz(quiz?.id, (quiz) => {
quiz.config.yandexMetricNumber = currentValue
? Number(currentValue)
: undefined;
});
handleCloseModal();
if (!currentValue) {
setIsSave(false);
return;
}
setIsSave(true);
};
const handleEdit = () => {
setIsSave(false);
};
const handleClear = () => {
setCurrentValue("");
setIsSave(false);
};
return (
<Dialog
open={isModalOpen}
onClose={handleCloseModal}
fullWidth
PaperProps={{
sx: {
maxWidth: isTablet ? "100%" : "580px",
maxHeight: isTablet ? "100%" : "251px",
borderRadius: "12px",
},
}}
>
<Box
sx={{
width: "100%",
height: "68px",
backgroundColor: theme.palette.background.default,
}}
>
<Typography
sx={{
fontSize: isMobile ? "20px" : "24px",
fontWeight: "500",
padding: "20px",
}}
>
Аналитика с Яндекс.Метрикой
</Typography>
</Box>
<IconButton
onClick={handleCloseModal}
sx={{
width: "12px",
height: "12px",
position: "absolute",
right: "15px",
top: "15px",
}}
>
<CloseIcon
sx={{ width: "12px", height: "12px", transform: "scale(1.5)" }}
/>
</IconButton>
<Box
sx={{
display: "flex",
flexDirection: "column",
padding: "15px 20px 15px",
flexGrow: 1,
gap: "20px",
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "10px",
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography fontWeight={500}>
{isSave ? "Ваш номер счетчика" : "Введите номер счетчика"}
</Typography>
{isSave && (
<Box>
<IconButton onClick={handleEdit}>
<EditPencil
color={theme.palette.brightPurple.main}
width={"18px"}
height={"18px"}
/>
</IconButton>
<IconButton onClick={handleClear}>
<Trash
sx={{
width: "24px",
"& path": {
stroke: theme.palette.brightPurple.main,
},
}}
/>
</IconButton>
</Box>
)}
</Box>
<CustomTextField
placeholder={isSave ? currentValue : "в формате ХХХХХХХХ"}
type={"number"}
value={currentValue}
disabled={isSave}
onChange={(e) => {
const onlyNums = e.target.value.replace(/[^0-9]/g, "");
setCurrentValue(onlyNums);
}}
/>
</Box>
{!isSave && (
<Box
sx={{
display: "flex",
justifyContent: isMobile ? "space-between" : "end",
gap: "10px",
}}
>
<Button
sx={{ width: isMobile ? "100%" : "130px" }}
onClick={handleCloseModal}
variant={"outlined"}
>
Отмена
</Button>
<Button
sx={{ width: isMobile ? "100%" : "130px" }}
variant={"contained"}
onClick={handleSave}
>
Сохранить
</Button>
</Box>
)}
</Box>
</Dialog>
);
}

@ -0,0 +1,144 @@
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { FC } from "react";
import { object, string } from "yup";
import InputTextfield from "@ui_kit/InputTextfield";
import PasswordInput from "@ui_kit/passwordInput";
import { useFormik } from "formik";
import { StepButtonsBlock } from "../StepButtonsBlock/StepButtonsBlock";
type IntegrationStep1Props = {
handleNextStep: () => void;
};
interface Values {
login: string;
password: string;
}
const initialValues: Values = {
login: "",
password: "",
};
const validationSchema = object({
login: string().required("Поле обязательно"),
password: string().required("Поле обязательно").min(8, "Минимум 8 символов"),
});
export const IntegrationStep1: FC<IntegrationStep1Props> = ({
handleNextStep,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const formik = useFormik<Values>({
initialValues,
validationSchema,
onSubmit: async (values, formikHelpers) => {
const loginTrimmed = values.login.trim();
const passwordTrimmed = values.password.trim();
try {
// Simulate a network request
await new Promise((resolve) => setTimeout(resolve, 2000));
handleNextStep();
} catch (error) {
formikHelpers.setSubmitting(false);
if (error instanceof Error) {
formikHelpers.setErrors({
login: error.message,
password: error.message,
});
}
}
},
});
return (
<Box
component="form"
onSubmit={formik.handleSubmit}
noValidate
sx={{
display: "flex",
flexDirection: "column",
alignItems: isMobile ? "start" : "center",
height: "100%",
flexGrow: 1,
}}
>
<Box
sx={{
marginTop: "68px",
width: isMobile ? "100%" : "500px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "15px",
}}
>
<InputTextfield
TextfieldProps={{
value: formik.values.login,
placeholder: "+7 900 000 00 00 или username@penahaub.com",
onBlur: formik.handleBlur,
error: formik.touched.login && Boolean(formik.errors.login),
helperText: formik.touched.login && formik.errors.login,
"data-cy": "login",
}}
onChange={formik.handleChange}
color={theme.palette.background.default}
id="login"
label="Телефон или E-mail"
gap="10px"
/>
<PasswordInput
TextfieldProps={{
value: formik.values.password,
placeholder: "Не менее 8 символов",
onBlur: formik.handleBlur,
error: formik.touched.password && Boolean(formik.errors.password),
helperText: formik.touched.password && formik.errors.password,
type: "password",
"data-cy": "password",
}}
onChange={formik.handleChange}
color={theme.palette.background.default}
id="password"
label="Пароль"
gap="10px"
/>
</Box>
<Box sx={{ marginTop: "30px", width: isMobile ? "100%" : "500px" }}>
<Typography
sx={{
fontSize: "16px",
fontWeight: "400",
color: theme.palette.grey2.main,
lineHeight: "1",
}}
>
Инструкция
</Typography>
<Typography
sx={{
marginTop: "12px",
fontSize: "18px",
fontWeight: "400",
color: theme.palette.grey3.main,
lineHeight: "1",
}}
>
Повседневная практика показывает, что постоянный количественный рост и
сфера нашей активности способствует подготовки и реализации систем
массового участия
</Typography>
</Box>
<StepButtonsBlock
isSmallBtnDisabled={true}
largeBtnType={"submit"}
isLargeBtnDisabled={formik.isSubmitting}
largeBtnText={"Войти"}
/>
</Box>
);
};

@ -0,0 +1,75 @@
import { Box, useMediaQuery, useTheme } from "@mui/material";
import { FC } from "react";
import { StepButtonsBlock } from "../StepButtonsBlock/StepButtonsBlock";
import { CustomSelect } from "../../../../components/CustomSelect/CustomSelect";
import { CustomRadioGroup } from "../../../../components/CustomRadioGroup/CustomRadioGroup";
type IntegrationStep2Props = {
handlePrevStep: () => void;
handleNextStep: () => void;
selectedFunnelPerformer: string | null;
setSelectedFunnelPerformer: (value: string | null) => void;
selectedFunnel: string | null;
setSelectedFunnel: (value: string | null) => void;
performers: string[];
funnels: string[];
};
export const IntegrationStep2: FC<IntegrationStep2Props> = ({
handlePrevStep,
handleNextStep,
selectedFunnelPerformer,
setSelectedFunnelPerformer,
selectedFunnel,
setSelectedFunnel,
performers,
funnels,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
height: "100%",
flexGrow: 1,
}}
>
<Box sx={{ width: "100%", marginTop: "20px", zIndex: 3 }}>
<CustomSelect
selectedItem={selectedFunnelPerformer}
items={performers}
setSelectedItem={setSelectedFunnelPerformer}
/>
</Box>
<Box
sx={{
marginTop: "20px",
flexGrow: 1,
width: "100%",
height: "346px",
}}
>
<CustomRadioGroup
items={funnels}
selectedValue={selectedFunnel}
setSelectedValue={setSelectedFunnel}
/>
</Box>
<Box
sx={{
marginTop: "20px",
alignSelf: "end",
}}
>
<StepButtonsBlock
onLargeBtnClick={handleNextStep}
onSmallBtnClick={handlePrevStep}
/>
</Box>
</Box>
);
};

@ -0,0 +1,76 @@
import { Box, useMediaQuery, useTheme } from "@mui/material";
import { FC } from "react";
import { StepButtonsBlock } from "../StepButtonsBlock/StepButtonsBlock";
import { CustomSelect } from "../../../../components/CustomSelect/CustomSelect";
import { CustomRadioGroup } from "../../../../components/CustomRadioGroup/CustomRadioGroup";
type IntegrationStep3Props = {
handlePrevStep: () => void;
handleNextStep: () => void;
selectedStagePerformer: string | null;
setSelectedStagePerformer: (value: string | null) => void;
selectedStage: string | null;
setSelectedStage: (value: string | null) => void;
performers: string[];
stages: string[];
};
export const IntegrationStep3: FC<IntegrationStep3Props> = ({
handlePrevStep,
handleNextStep,
selectedStagePerformer,
setSelectedStagePerformer,
selectedStage,
setSelectedStage,
performers,
stages,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
height: "100%",
flexGrow: 1,
}}
>
<Box sx={{ width: "100%", marginTop: "20px", zIndex: 3 }}>
<CustomSelect
selectedItem={selectedStagePerformer}
items={performers}
setSelectedItem={setSelectedStagePerformer}
/>
</Box>
<Box
sx={{
marginTop: "20px",
flexGrow: 1,
width: "100%",
height: "346px",
}}
>
<CustomRadioGroup
items={stages}
selectedValue={selectedStage}
setSelectedValue={setSelectedStage}
/>
</Box>
<Box
sx={{
marginTop: "20px",
alignSelf: "end",
}}
>
<StepButtonsBlock
onLargeBtnClick={handleNextStep}
onSmallBtnClick={handlePrevStep}
/>
</Box>
</Box>
);
};

@ -0,0 +1,55 @@
import { Box, useMediaQuery, useTheme } from "@mui/material";
import { FC } from "react";
import { StepButtonsBlock } from "../StepButtonsBlock/StepButtonsBlock";
import { CustomSelect } from "../../../../components/CustomSelect/CustomSelect";
type IntegrationStep4Props = {
handlePrevStep: () => void;
handleNextStep: () => void;
selectedDealPerformer: string | null;
setSelectedDealPerformer: (value: string | null) => void;
performers: string[];
};
export const IntegrationStep4: FC<IntegrationStep4Props> = ({
handlePrevStep,
handleNextStep,
selectedDealPerformer,
setSelectedDealPerformer,
performers,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
height: "100%",
flexGrow: 1,
}}
>
<Box sx={{ width: "100%", marginTop: "20px", zIndex: 3 }}>
<CustomSelect
selectedItem={selectedDealPerformer}
items={performers}
setSelectedItem={setSelectedDealPerformer}
/>
</Box>
<Box
sx={{
marginTop: "auto",
alignSelf: "end",
}}
>
<StepButtonsBlock
onLargeBtnClick={handleNextStep}
onSmallBtnClick={handlePrevStep}
/>
</Box>
</Box>
);
};

@ -0,0 +1,94 @@
import {
Box,
ButtonBase,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import UploadIcon from "@icons/UploadIcon";
import { type DragEvent, FC, useRef, useState } from "react";
type TextFormat = "txt" | "docx";
interface CustomFileUploaderProps {
description?: string;
accept?: TextFormat[];
handleImageChange: (file: File) => void;
}
export const CustomFileUploader: FC<CustomFileUploaderProps> = ({
accept,
description,
handleImageChange,
}) => {
const theme = useTheme();
const dropZone = useRef<HTMLDivElement>(null);
const [ready, setReady] = useState(false);
const isMobile = useMediaQuery(theme.breakpoints.down(700));
const handleDragEnter = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
setReady(true);
};
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
const file = event.dataTransfer.files[0];
if (!file) return;
handleImageChange(file);
};
const acceptedFormats = accept
? accept.map((format) => "." + format).join(", ")
: "";
return (
<ButtonBase component="label" sx={{ justifyContent: "flex-start" }}>
<input
onChange={(event) => {
const file = event.target.files?.[0];
if (file) handleImageChange(file);
}}
hidden
accept={acceptedFormats || ".jpg, .jpeg, .png , .gif"}
multiple
type="file"
data-cy="upload-image-input"
/>
<Box
onDragOver={(event: DragEvent<HTMLDivElement>) =>
event.preventDefault()
}
onDrop={handleDrop}
ref={dropZone}
sx={{
width: isMobile ? "100%" : "580px",
padding: isMobile ? "33px" : "33px 10px 33px 55px",
display: "flex",
alignItems: "center",
backgroundColor: theme.palette.background.default,
border: `1px solid ${ready ? "red" : theme.palette.grey2.main}`,
borderRadius: "8px",
gap: "55px",
flexDirection: isMobile ? "column" : "row",
}}
onDragEnter={handleDragEnter}
>
<UploadIcon />
<Box>
<Typography sx={{ color: "#9A9AAF", fontWeight: "bold" }}>
Добавить файл
</Typography>
<Typography
sx={{ color: theme.palette.grey2.main, fontSize: "16px" }}
>
{description || "Принимает JPG, PNG, и GIF формат — максимум 5mb"}
</Typography>
</Box>
</Box>
</ButtonBase>
);
};

@ -0,0 +1,58 @@
import React, { FC } from "react";
import Box from "@mui/material/Box";
import { IconButton, Typography, useTheme } from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
type FileBlockProps = {
file: File | null;
setFile?: (file: File | null) => void;
};
export const FileBlock: FC<FileBlockProps> = ({ setFile, file }) => {
const theme = useTheme();
return (
<Box sx={{ display: "flex", gap: "15px", alignItems: "center" }}>
<Typography
sx={{
fontSize: "18px",
fontWeight: "400",
color: theme.palette.grey3.main,
}}
>
Вы загрузили:
</Typography>
<Box
sx={{
display: "flex",
alignItems: "center",
backgroundColor: theme.palette.brightPurple.main,
borderRadius: "8px",
padding: setFile ? "5px 5px 5px 14px" : "5px 14px",
gap: "20px",
}}
>
<Typography
sx={{ color: "white", fontSize: "14px", fontWeight: "400" }}
>
{file?.name}
</Typography>
{setFile && (
<IconButton
onClick={() => setFile(null)}
sx={{
backgroundColor: "#864BD9",
borderRadius: "50%",
width: "24px",
height: "24px",
color: "white",
}}
>
<CloseIcon
sx={{ width: "14px", height: "14px", transform: "scale(1.5)" }}
/>
</IconButton>
)}
</Box>
</Box>
);
};

@ -0,0 +1,52 @@
import { Box, useMediaQuery, useTheme } from "@mui/material";
import React, { FC } from "react";
import { StepButtonsBlock } from "../StepButtonsBlock/StepButtonsBlock";
import File from "@ui_kit/QuizPreview/QuizPreviewQuestionTypes/File";
import { FileBlock } from "./FileBlock/FileBlock";
import { CustomFileUploader } from "./CustomFileUploader/CustomFileUploader";
type IntegrationStep5Props = {
handlePrevStep: () => void;
handleNextStep: () => void;
setUtmFile: (file: File | null) => void;
utmFile: File | null;
};
export const IntegrationStep5: FC<IntegrationStep5Props> = ({
handlePrevStep,
handleNextStep,
utmFile,
setUtmFile,
}) => {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
height: "100%",
flexGrow: 1,
}}
>
<Box sx={{ alignSelf: "start", marginTop: "20px" }}>
{utmFile ? (
<FileBlock file={utmFile} setFile={setUtmFile} />
) : (
<CustomFileUploader
description={"Принимает .txt и .docx формат — максимум 100мб"}
accept={["txt", "docx"]}
handleImageChange={setUtmFile}
/>
)}
</Box>
<StepButtonsBlock
onLargeBtnClick={handleNextStep}
onSmallBtnClick={handlePrevStep}
isLargeBtnDisabled={!utmFile}
/>
</Box>
);
};

@ -0,0 +1,82 @@
import { useTheme } from "@mui/material";
import {
Dispatch,
FC,
SetStateAction,
useCallback,
useMemo,
useState,
} from "react";
import { ItemsSelectionView } from "./ItemsSelectionView/ItemsSelectionView";
import { ItemDetailsView } from "./ItemDetailsView/ItemDetailsView";
import { TitleKeys, TQuestionEntity } from "../IntegrationsModal";
import Box from "@mui/material/Box";
type IntegrationStep6Props = {
handlePrevStep: () => void;
handleNextStep: () => void;
questionEntity: TQuestionEntity;
setQuestionEntity: Dispatch<SetStateAction<TQuestionEntity>>;
};
export const IntegrationStep6: FC<IntegrationStep6Props> = ({
handlePrevStep,
handleNextStep,
questionEntity,
setQuestionEntity,
}) => {
const theme = useTheme();
const [isSelection, setIsSelection] = useState<boolean>(false);
const [activeItem, setActiveItem] = useState<string | null>(null);
const [selectedValue, setSelectedValue] = useState<string | null>(null);
const handleAdd = useCallback(() => {
if (!activeItem || !selectedValue) return;
setQuestionEntity((prevState) => ({
...prevState,
[activeItem]: [...prevState[activeItem as TitleKeys], selectedValue],
}));
}, [activeItem, setQuestionEntity, selectedValue]);
const items = useMemo(
() => ["Город", "Имя", "Фамилия", "Отчество", "Контрагент"],
[],
);
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
height: "100%",
flexGrow: 1,
}}
>
{isSelection ? (
<ItemsSelectionView
items={items}
selectedValue={selectedValue}
setSelectedValue={setSelectedValue}
onSmallBtnClick={() => {
setActiveItem(null);
setIsSelection(false);
}}
onLargeBtnClick={() => {
handleAdd();
setActiveItem(null);
setIsSelection(false);
}}
/>
) : (
<ItemDetailsView
setIsSelection={setIsSelection}
handleNextStep={handleNextStep}
handlePrevStep={handlePrevStep}
questionEntity={questionEntity}
setActiveItem={setActiveItem}
/>
)}
</Box>
);
};

@ -0,0 +1,39 @@
import { Box, Typography, useTheme } from "@mui/material";
import { FC } from "react";
type AnswerItemProps = {
fieldName: string;
fieldValue: string;
};
export const AnswerItem: FC<AnswerItemProps> = ({ fieldName, fieldValue }) => {
const theme = useTheme();
return (
<Box
sx={{
padding: "10px 20px",
height: "140px",
borderBottom: `1px solid ${theme.palette.background.default}`,
}}
>
<Typography
sx={{
fontSize: "14px",
fontWeight: 500,
color: theme.palette.grey3.main,
}}
>
{fieldName}
</Typography>
<Typography
sx={{
fontSize: "14px",
fontWeight: 400,
color: theme.palette.grey3.main,
}}
>
{fieldValue}
</Typography>
</Box>
);
};

@ -0,0 +1,46 @@
import { Box, IconButton, useTheme } from "@mui/material";
import AddPlus from "@icons/questionsPage/addPlus";
import { FC } from "react";
type IconBtnAddProps = {
onAddBtnClick: () => void;
};
export const IconBtnAdd: FC<IconBtnAddProps> = ({ onAddBtnClick }) => {
const theme = useTheme();
return (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
borderBottom: `1px solid ${theme.palette.background.default}`,
}}
>
<IconButton
onClick={onAddBtnClick}
sx={{
width: "fit-content",
marginTop: "20px",
marginBottom: "66px",
circle: {
fill: "#EEE4FC",
},
"&:hover": {
circle: {
fill: theme.palette.brightPurple.main,
},
},
"&:active": {
circle: {
fill: "#581CA7",
},
},
}}
>
<AddPlus />
</IconButton>
</Box>
);
};

@ -0,0 +1,69 @@
import { Box, Typography, useTheme } from "@mui/material";
import { FC } from "react";
import { IconBtnAdd } from "./IconBtnAdd/IconBtnAdd";
import { AnswerItem } from "./AnswerItem/AnswerItem";
import {
TagKeys,
TitleKeys,
TQuestionEntity,
TTags,
} from "../../IntegrationsModal";
type ItemProps = {
title: TitleKeys | TagKeys;
onAddBtnClick: () => void;
data: TQuestionEntity | TTags;
};
export const Item: FC<ItemProps> = ({ title, onAddBtnClick, data }) => {
const theme = useTheme();
const titleDictionary = {
contact: "Контакт",
company: "Компания",
deal: "Сделка",
buyer: "Покупатель",
contacts: "Контакты",
users: "Пользователи",
buyers: "Покупатели",
};
const translatedTitle = titleDictionary[title];
const selectedOptions = data[title];
return (
<Box
sx={{
width: "172px",
display: "flex",
flexDirection: "column",
borderRight: `1px solid ${theme.palette.background.default}`,
}}
>
<Box
sx={{
alignSelf: "center",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "12px",
backgroundColor: theme.palette.background.default,
width: "156px",
height: "40px",
}}
>
<Typography sx={{ fontSize: "16px", fontWeight: 500 }}>
{translatedTitle}
</Typography>
</Box>
{selectedOptions &&
selectedOptions.map((text, index) => (
<AnswerItem
key={text + index}
fieldValue={"Значение поля"}
fieldName={text}
/>
))}
<IconBtnAdd onAddBtnClick={onAddBtnClick} />
</Box>
);
};

@ -0,0 +1,78 @@
import { Box, useTheme } from "@mui/material";
import { Item } from "../Item/Item";
import { StepButtonsBlock } from "../../StepButtonsBlock/StepButtonsBlock";
import { FC } from "react";
import { TQuestionEntity } from "../../IntegrationsModal";
type TitleKeys = "contacts" | "company" | "deal" | "users" | "buyers";
type ItemDetailsViewProps = {
setIsSelection: (value: boolean) => void;
handlePrevStep: () => void;
handleNextStep: () => void;
questionEntity: TQuestionEntity;
setActiveItem: (value: string | null) => void;
};
export const ItemDetailsView: FC<ItemDetailsViewProps> = ({
handlePrevStep,
handleNextStep,
questionEntity,
setActiveItem,
setIsSelection,
}) => {
const theme = useTheme();
return (
<Box
sx={{
marginTop: "20px",
display: "flex",
flexDirection: "column",
alignItems: "center",
height: "100%",
flexGrow: 1,
}}
>
<Box
sx={{
width: "100%",
height: "400px",
flexGrow: 1,
borderRadius: "10px",
padding: "10px",
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
display: "flex",
overflowY: "auto",
flexWrap: "wrap",
justifyContent: "start",
}}
>
{questionEntity &&
Object.keys(questionEntity).map((item) => (
<Item
key={item}
title={item as TitleKeys}
onAddBtnClick={() => {
setIsSelection(true);
setActiveItem(item);
}}
data={questionEntity}
/>
))}
</Box>
<Box
sx={{
marginTop: "20px",
alignSelf: "end",
}}
>
<StepButtonsBlock
onSmallBtnClick={handlePrevStep}
onLargeBtnClick={handleNextStep}
/>
</Box>
</Box>
);
};

@ -0,0 +1,60 @@
import { Box } from "@mui/material";
import { CustomRadioGroup } from "../../../../../components/CustomRadioGroup/CustomRadioGroup";
import { StepButtonsBlock } from "../../StepButtonsBlock/StepButtonsBlock";
import { FC } from "react";
type ItemsSelectionViewProps = {
items: string[];
selectedValue: string | null;
setSelectedValue: (value: string | null) => void;
onLargeBtnClick: () => void;
onSmallBtnClick: () => void;
};
export const ItemsSelectionView: FC<ItemsSelectionViewProps> = ({
items,
selectedValue,
setSelectedValue,
onLargeBtnClick,
onSmallBtnClick,
}) => {
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
height: "100%",
flexGrow: 1,
}}
>
<Box
sx={{
marginTop: "20px",
flexGrow: 1,
width: "100%",
height: "346px",
}}
>
<CustomRadioGroup
items={items}
selectedValue={selectedValue}
setSelectedValue={setSelectedValue}
/>
</Box>
<Box
sx={{
marginTop: "20px",
alignSelf: "end",
}}
>
<StepButtonsBlock
onLargeBtnClick={onLargeBtnClick}
largeBtnText={"Добавить"}
onSmallBtnClick={onSmallBtnClick}
smallBtnText={"Отменить"}
/>
</Box>
</Box>
);
};

@ -0,0 +1,83 @@
import { useTheme } from "@mui/material";
import {
Dispatch,
FC,
SetStateAction,
useCallback,
useMemo,
useState,
} from "react";
import { TagKeys, TTags } from "../IntegrationsModal";
import Box from "@mui/material/Box";
import { ItemsSelectionView } from "../IntegrationStep6/ItemsSelectionView/ItemsSelectionView";
import { TagsDetailsView } from "./TagsDetailsView/TagsDetailsView";
type IntegrationStep7Props = {
handleSmallBtn: () => void;
handleLargeBtn: () => void;
tags: TTags;
setTags: Dispatch<SetStateAction<TTags>>;
};
export const IntegrationStep7: FC<IntegrationStep7Props> = ({
handleSmallBtn,
handleLargeBtn,
tags,
setTags,
}) => {
const theme = useTheme();
const [isSelection, setIsSelection] = useState<boolean>(false);
const [activeItem, setActiveItem] = useState<string | null>(null);
const [selectedValue, setSelectedValue] = useState<string | null>(null);
const handleAdd = useCallback(() => {
if (!activeItem || !selectedValue) return;
setTags((prevState) => ({
...prevState,
[activeItem]: [...prevState[activeItem as TagKeys], selectedValue],
}));
}, [activeItem, setTags, selectedValue]);
const items = useMemo(
() => ["#тег с результатом 1", "#еще один тег с результатом 2", "#тег"],
[],
);
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
height: "100%",
flexGrow: 1,
}}
>
{isSelection ? (
<ItemsSelectionView
items={items}
selectedValue={selectedValue}
setSelectedValue={setSelectedValue}
onSmallBtnClick={() => {
setActiveItem(null);
setIsSelection(false);
}}
onLargeBtnClick={() => {
handleAdd();
setActiveItem(null);
setIsSelection(false);
}}
/>
) : (
<TagsDetailsView
setIsSelection={setIsSelection}
handleLargeBtn={handleLargeBtn}
handleSmallBtn={handleSmallBtn}
tags={tags}
setActiveItem={setActiveItem}
/>
)}
</Box>
);
};

@ -0,0 +1,99 @@
import { Box, Typography, useTheme } from "@mui/material";
import { StepButtonsBlock } from "../../StepButtonsBlock/StepButtonsBlock";
import { FC } from "react";
import { TagKeys, TTags } from "../../IntegrationsModal";
import { Item } from "../../IntegrationStep6/Item/Item";
type TagsDetailsViewProps = {
setIsSelection: (value: boolean) => void;
handleSmallBtn: () => void;
handleLargeBtn: () => void;
tags: TTags;
setActiveItem: (value: string | null) => void;
};
export const TagsDetailsView: FC<TagsDetailsViewProps> = ({
handleSmallBtn,
handleLargeBtn,
tags,
setActiveItem,
setIsSelection,
}) => {
const theme = useTheme();
return (
<Box
sx={{
marginTop: "20px",
display: "flex",
flexDirection: "column",
alignItems: "center",
height: "100%",
flexGrow: 1,
}}
>
<Box
sx={{
width: "100%",
height: "400px",
flexGrow: 1,
borderRadius: "10px",
padding: "10px",
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
display: "flex",
}}
>
<Box
sx={{
padding: "0 40px",
borderRight: `1px solid ${theme.palette.background.default}`,
height: "100%",
display: "flex",
alignItems: "center",
}}
>
<Typography
sx={{ fontSize: "14px", color: theme.palette.grey2.main }}
>
Результат
</Typography>
</Box>
<Box
sx={{
width: "100%",
flexGrow: 1,
display: "flex",
overflowY: "auto",
flexWrap: "wrap",
justifyContent: "start",
}}
>
{tags &&
Object.keys(tags).map((item) => (
<Item
key={item}
title={item as TagKeys}
onAddBtnClick={() => {
setIsSelection(true);
setActiveItem(item);
}}
data={tags}
/>
))}
</Box>
</Box>
<Box
sx={{
marginTop: "20px",
alignSelf: "end",
}}
>
<StepButtonsBlock
onSmallBtnClick={handleSmallBtn}
onLargeBtnClick={handleLargeBtn}
largeBtnText={"Сохранить"}
/>
</Box>
</Box>
);
};

@ -0,0 +1,270 @@
import {
Dialog,
IconButton,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import React, { FC, useMemo, useState } from "react";
import Box from "@mui/material/Box";
import CloseIcon from "@mui/icons-material/Close";
import { IntegrationStep1 } from "./IntegrationStep1/IntegrationStep1";
import { IntegrationStep2 } from "./IntegrationStep2/IntegrationStep2";
import { IntegrationStep3 } from "./IntegrationStep3/IntegrationStep3";
import { IntegrationStep4 } from "./IntegrationStep4/IntegrationStep4";
import { IntegrationStep5 } from "./IntegrationStep5/IntegrationStep5";
import { IntegrationStep6 } from "./IntegrationStep6/IntegrationStep6";
import { funnelsMock, performersMock, stagesMock } from "../mocks/MockData";
import File from "@ui_kit/QuizPreview/QuizPreviewQuestionTypes/File";
import { IntegrationsModalTitle } from "./IntegrationsModalTitle/IntegrationsModalTitle";
import { SettingsBlock } from "./SettingsBlock/SettingsBlock";
import { IntegrationStep7 } from "./IntegrationStep7/IntegrationStep7";
export type TitleKeys = "contacts" | "company" | "deal" | "users" | "buyers";
export type TQuestionEntity = Record<TitleKeys, string[] | []>;
type IntegrationsModalProps = {
isModalOpen: boolean;
handleCloseModal: () => void;
companyName: string | null;
};
export type TagKeys = "contact" | "company" | "deal" | "buyer";
export type TTags = Record<TagKeys, string[] | []>;
export const IntegrationsModal: FC<IntegrationsModalProps> = ({
isModalOpen,
handleCloseModal,
companyName,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const [step, setStep] = useState<number>(0);
const [isSettingsBlock, setIsSettingsBlock] = useState<boolean>(false);
const [selectedFunnelPerformer, setSelectedFunnelPerformer] = useState<
string | null
>(null);
const [selectedFunnel, setSelectedFunnel] = useState<string | null>(null);
const [selectedStagePerformer, setSelectedStagePerformer] = useState<
string | null
>(null);
const [selectedStage, setSelectedStage] = useState<string | null>(null);
const [selectedDealPerformer, setSelectedDealPerformer] = useState<
string | null
>(null);
const [utmFile, setUtmFile] = useState<File | null>(null);
const [questionEntity, setQuestionEntity] = useState<TQuestionEntity>({
contacts: [],
company: [],
deal: [],
users: [],
buyers: [],
});
const [tags, setTags] = useState<TTags>({
deal: [],
contact: [],
company: [],
buyer: [],
});
const handleNextStep = () => {
setStep((prevState) => prevState + 1);
};
const handlePrevStep = () => {
setStep((prevState) => prevState - 1);
};
const handleSave = () => {
handleCloseModal();
setStep(1);
};
const steps = useMemo(
() => [
{
title: "Авторизация в аккаунте",
isSettingsAvailable: false,
component: <IntegrationStep1 handleNextStep={handleNextStep} />,
},
{
title: "Выбор воронки",
isSettingsAvailable: true,
component: (
<IntegrationStep2
handlePrevStep={handlePrevStep}
handleNextStep={handleNextStep}
selectedFunnelPerformer={selectedFunnelPerformer}
setSelectedFunnelPerformer={setSelectedFunnelPerformer}
selectedFunnel={selectedFunnel}
setSelectedFunnel={setSelectedFunnel}
performers={performersMock}
funnels={funnelsMock}
/>
),
},
{
title: "Выбор этапа воронки",
isSettingsAvailable: true,
component: (
<IntegrationStep3
handlePrevStep={handlePrevStep}
handleNextStep={handleNextStep}
selectedStagePerformer={selectedStagePerformer}
setSelectedStagePerformer={setSelectedStagePerformer}
selectedStage={selectedStage}
setSelectedStage={setSelectedStage}
performers={performersMock}
stages={stagesMock}
/>
),
},
{
title: "Сделка",
isSettingsAvailable: true,
component: (
<IntegrationStep4
handlePrevStep={handlePrevStep}
handleNextStep={handleNextStep}
selectedDealPerformer={selectedDealPerformer}
setSelectedDealPerformer={setSelectedDealPerformer}
performers={performersMock}
/>
),
},
{
title: "Добавление utm-меток",
isSettingsAvailable: false,
component: (
<IntegrationStep5
handlePrevStep={handlePrevStep}
handleNextStep={handleNextStep}
utmFile={utmFile}
setUtmFile={setUtmFile}
/>
),
},
{
title: "Соотнесение вопросов и сущностей",
isSettingsAvailable: true,
component: (
<IntegrationStep6
questionEntity={questionEntity}
setQuestionEntity={setQuestionEntity}
handlePrevStep={handlePrevStep}
handleNextStep={handleNextStep}
/>
),
},
{
title: "Добавление тегов",
isSettingsAvailable: true,
component: (
<IntegrationStep7
handleSmallBtn={handlePrevStep}
handleLargeBtn={handleSave}
tags={tags}
setTags={setTags}
/>
),
},
],
[
questionEntity,
utmFile,
selectedFunnelPerformer,
selectedFunnel,
selectedStagePerformer,
selectedStage,
selectedDealPerformer,
tags,
],
);
const stepTitles = steps.map((step) => step.title);
return (
<Dialog
open={isModalOpen}
onClose={handleCloseModal}
fullWidth
fullScreen={isMobile}
PaperProps={{
sx: {
maxWidth: isTablet ? "100%" : "920px",
maxHeight: isTablet ? "100%" : "660px",
borderRadius: "12px",
},
}}
>
<Box
sx={{
width: "100%",
height: "68px",
backgroundColor: theme.palette.background.default,
}}
>
<Typography
sx={{
fontSize: isMobile ? "20px" : "24px",
fontWeight: "500",
padding: "20px",
}}
>
Интеграция с {companyName ? companyName : "партнером"}
</Typography>
</Box>
<IconButton
onClick={handleCloseModal}
sx={{
width: "12px",
height: "12px",
position: "absolute",
right: "15px",
top: "15px",
}}
>
<CloseIcon
sx={{ width: "12px", height: "12px", transform: "scale(1.5)" }}
/>
</IconButton>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: isTablet ? "100%" : "920px",
height: "600px",
padding: "15px 20px 15px",
flexGrow: 1,
}}
>
<IntegrationsModalTitle
step={step}
steps={steps}
isSettingsBlock={isSettingsBlock}
setIsSettingsBlock={setIsSettingsBlock}
setStep={setStep}
/>
{isSettingsBlock ? (
<Box sx={{ flexGrow: 1, width: "100%" }}>
<SettingsBlock
stepTitles={stepTitles}
setIsSettingsBlock={setIsSettingsBlock}
setStep={setStep}
selectedDealPerformer={selectedDealPerformer}
selectedFunnelPerformer={selectedFunnelPerformer}
selectedFunnel={selectedFunnel}
selectedStagePerformer={selectedStagePerformer}
selectedStage={selectedStage}
utmFile={utmFile}
questionEntity={questionEntity}
tags={tags}
/>
</Box>
) : (
<Box sx={{ flexGrow: 1, width: "100%" }}>{steps[step].component}</Box>
)}
</Box>
</Dialog>
);
};

@ -0,0 +1,120 @@
import Box from "@mui/material/Box";
import { Button, Typography, useMediaQuery, useTheme } from "@mui/material";
import GearIcon from "@icons/GearIcon";
import React, { FC, useCallback, useMemo } from "react";
import AccountSetting from "@icons/AccountSetting";
type IntegrationsModalTitleProps = {
step: number;
steps: { title: string; isSettingsAvailable: boolean }[];
isSettingsBlock?: boolean;
setIsSettingsBlock: (value: boolean) => void;
setStep: (value: number) => void;
};
export const IntegrationsModalTitle: FC<IntegrationsModalTitleProps> = ({
step,
steps,
setIsSettingsBlock,
isSettingsBlock,
setStep,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const handleClick = useCallback(() => {
if (isSettingsBlock) {
setIsSettingsBlock(false);
setStep(0);
return;
}
setIsSettingsBlock(true);
}, [isSettingsBlock, setIsSettingsBlock, setStep]);
const btnText = useMemo(() => {
return isSettingsBlock ? "Сменить аккаунт" : "Мои настройки";
}, [isSettingsBlock]);
return (
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
<Box>
<Typography
sx={{
fontSize: isMobile ? "18px" : "24px",
color: theme.palette.grey3.main,
fontWeight: "400",
lineHeight: "1",
}}
>
{isSettingsBlock ? "Мои настройки" : steps[step].title}
</Typography>
{isSettingsBlock || (
<Typography
sx={{
color: theme.palette.grey2.main,
fontWeight: "400",
marginTop: "4px",
fontSize: "14px",
lineHeight: "1",
}}
>
Шаг {step + 1}
</Typography>
)}
</Box>
{steps[step].isSettingsAvailable && (
<Button
variant="outlined"
startIcon={
isSettingsBlock ? (
<AccountSetting
color={theme.palette.brightPurple.main}
height={"20px"}
width={"20px"}
/>
) : (
<GearIcon
color={theme.palette.brightPurple.main}
height={"24px"}
width={"24px"}
/>
)
}
onClick={handleClick}
sx={{
padding: isMobile ? "10px" : "10px 20px",
width: "fit-content",
backgroundColor: "transparent",
color: theme.palette.brightPurple.main,
"& .MuiButton-startIcon": {
marginRight: isMobile ? 0 : "8px",
marginLeft: 0,
},
"&:hover": {
backgroundColor: theme.palette.brightPurple.main,
color: theme.palette.common.white,
"& path": {
stroke: theme.palette.common.white,
},
"& circle": {
stroke: theme.palette.common.white,
},
},
"&:active": {
backgroundColor: "#581CA7",
color: theme.palette.common.white,
"& path": {
stroke: theme.palette.common.white,
},
"& circle": {
stroke: theme.palette.common.white,
},
},
}}
>
{isMobile ? "" : btnText}
</Button>
)}
</Box>
);
};

@ -0,0 +1,38 @@
import { Typography, useTheme } from "@mui/material";
import { FC } from "react";
type ResponsiblePersonProps = {
performer: string | null;
};
export const ResponsiblePerson: FC<ResponsiblePersonProps> = ({
performer,
}) => {
const theme = useTheme();
return (
<>
<Typography
sx={{
color: theme.palette.grey2.main,
fontSize: "18px",
fontWeight: 400,
margin: "10px 8px 0 0",
}}
display={"inline-block"}
>
Ответственный за сделку:
</Typography>
<Typography
sx={{
color: theme.palette.grey3.main,
fontSize: "18px",
fontWeight: 400,
}}
display={"inline"}
>
{performer ? performer : "Не выбран"}
</Typography>
</>
);
};

@ -0,0 +1,27 @@
import { Typography, useTheme } from "@mui/material";
import Box from "@mui/material/Box";
import { FC } from "react";
type SelectedParameterProps = {
parameter: string | null;
};
export const SelectedParameter: FC<SelectedParameterProps> = ({
parameter,
}) => {
const theme = useTheme();
return (
<Box
sx={{
display: "flex",
width: "100%",
padding: "15px 20px",
backgroundColor: theme.palette.background.default,
borderRadius: "12px",
marginTop: "10px",
}}
>
<Typography>{parameter ? parameter : "Не выбрано"}</Typography>
</Box>
);
};

@ -0,0 +1,173 @@
import Box from "@mui/material/Box";
import { FC, useMemo } from "react";
import { Typography, useMediaQuery, useTheme } from "@mui/material";
import { SettingItemHeader } from "./SettingItemHeader/SettingItemHeader";
import { ResponsiblePerson } from "./ResponsiblePerson/ResponsiblePerson";
import { SelectedParameter } from "./SelectedParameter/SelectedParameter";
import { FileBlock } from "../../IntegrationStep5/FileBlock/FileBlock";
import { TQuestionEntity, TTags } from "../../IntegrationsModal";
type SettingItemProps = {
step: number;
title: string;
setStep: (value: number) => void;
setIsSettingsBlock: (value: boolean) => void;
selectedFunnelPerformer: string | null;
selectedFunnel: string | null;
selectedStagePerformer: string | null;
selectedDealPerformer: string | null;
selectedStage: string | null;
utmFile: File | null;
questionEntity: TQuestionEntity;
tags: TTags;
};
export const SettingItem: FC<SettingItemProps> = ({
step,
title,
setStep,
setIsSettingsBlock,
selectedFunnelPerformer,
selectedFunnel,
selectedStagePerformer,
selectedDealPerformer,
selectedStage,
utmFile,
questionEntity,
tags,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
if (step === 0) {
return;
}
const SettingsContent = useMemo(() => {
if (step === 1) {
return (
<>
<ResponsiblePerson performer={selectedFunnelPerformer} />
<SelectedParameter parameter={selectedFunnel} />
</>
);
}
if (step === 2) {
return (
<>
<ResponsiblePerson performer={selectedStagePerformer} />
<SelectedParameter parameter={selectedStage} />
</>
);
}
if (step === 3) {
return (
<>
<ResponsiblePerson performer={selectedDealPerformer} />
</>
);
}
if (step === 4) {
return (
<Box sx={{ display: "flex", gap: "15px", marginTop: "20px" }}>
{utmFile ? (
<FileBlock file={utmFile} />
) : (
<Typography>Файл не загружен</Typography>
)}
</Box>
);
}
if (step === 5) {
const isFilled = Object.values(questionEntity).some(
(array) => array.length > 0,
);
const status = isFilled ? "Заполнено" : "Не заполнено";
return (
<>
<Typography
sx={{
color: theme.palette.grey2.main,
fontSize: "18px",
fontWeight: 400,
margin: "10px 8px 0 0",
}}
display={"inline-block"}
>
Статус:
</Typography>
<Typography
sx={{
color: theme.palette.grey3.main,
fontSize: "18px",
fontWeight: 400,
}}
display={"inline"}
>
{status}
</Typography>
</>
);
}
if (step === 6) {
const isFilled = Object.values(tags).some((array) => array.length > 0);
const status = isFilled ? "Заполнено" : "Не заполнено";
return (
<>
<Typography
sx={{
color: theme.palette.grey2.main,
fontSize: "18px",
fontWeight: 400,
margin: "10px 8px 0 0",
}}
display={"inline-block"}
>
Статус:
</Typography>
<Typography
sx={{
color: theme.palette.grey3.main,
fontSize: "18px",
fontWeight: 400,
}}
display={"inline"}
>
{status}
</Typography>
</>
);
}
return null;
}, [
step,
selectedFunnelPerformer,
selectedFunnel,
selectedStagePerformer,
selectedDealPerformer,
selectedStage,
utmFile,
questionEntity,
tags,
]);
return (
<Box
sx={{
width: "100%",
padding: "20px 0",
borderTop: `1px solid ${theme.palette.background.default}`,
}}
>
<SettingItemHeader
title={title}
step={step}
setIsSettingsBlock={setIsSettingsBlock}
setStep={setStep}
/>
<Box>{SettingsContent}</Box>
</Box>
);
};

@ -0,0 +1,64 @@
import Box from "@mui/material/Box";
import { IconButton, Typography, useTheme } from "@mui/material";
import EditPencil from "@icons/EditPencil";
import { FC } from "react";
type SettingItemHeaderProps = {
title: string;
step: number;
setStep: (value: number) => void;
setIsSettingsBlock: (value: boolean) => void;
};
export const SettingItemHeader: FC<SettingItemHeaderProps> = ({
title,
step,
setStep,
setIsSettingsBlock,
}) => {
const theme = useTheme();
const handleClick = () => {
setStep(step);
setIsSettingsBlock(false);
};
return (
<Box>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Typography
sx={{
color: theme.palette.grey2.main,
fontSize: "14px",
fontWeight: 400,
}}
>
{step} этап
</Typography>
<IconButton onClick={handleClick}>
<EditPencil
color={theme.palette.brightPurple.main}
width={"18px"}
height={"18px"}
/>
</IconButton>
</Box>
<Typography
sx={{
color: theme.palette.grey3.main,
fontSize: "18px",
fontWeight: 500,
lineHeight: "1",
}}
>
{title}
</Typography>
</Box>
);
};

@ -0,0 +1,90 @@
import { FC } from "react";
import { Box, useMediaQuery, useTheme } from "@mui/material";
import { StepButtonsBlock } from "../StepButtonsBlock/StepButtonsBlock";
import { SettingItem } from "./SettingItem/SettingItem";
import { TQuestionEntity, TTags } from "../IntegrationsModal";
type SettingsBlockProps = {
stepTitles: string[];
setStep: (value: number) => void;
setIsSettingsBlock: (value: boolean) => void;
selectedFunnelPerformer: string | null;
selectedFunnel: string | null;
selectedStagePerformer: string | null;
selectedStage: string | null;
selectedDealPerformer: string | null;
utmFile: File | null;
questionEntity: TQuestionEntity;
tags: TTags;
};
export const SettingsBlock: FC<SettingsBlockProps> = ({
stepTitles,
setStep,
setIsSettingsBlock,
selectedFunnelPerformer,
selectedFunnel,
selectedStagePerformer,
selectedDealPerformer,
selectedStage,
utmFile,
questionEntity,
tags,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
height: "100%",
flexGrow: 1,
}}
>
<Box
sx={{
marginTop: "10px",
width: "100%",
height: "443px",
borderRadius: "10px",
padding: " 0 20px",
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
overflowY: "auto",
flexGrow: 1,
}}
>
{stepTitles &&
stepTitles.map((title, index) => (
<SettingItem
step={index}
title={title}
setIsSettingsBlock={setIsSettingsBlock}
setStep={setStep}
selectedFunnelPerformer={selectedFunnelPerformer}
selectedFunnel={selectedFunnel}
selectedStagePerformer={selectedStagePerformer}
selectedDealPerformer={selectedDealPerformer}
selectedStage={selectedStage}
utmFile={utmFile}
questionEntity={questionEntity}
tags={tags}
/>
))}
</Box>
<Box
sx={{
marginTop: "20px",
alignSelf: "end",
}}
>
<StepButtonsBlock
onSmallBtnClick={() => setIsSettingsBlock(false)}
isLargeBtnMissing={true}
/>
</Box>
</Box>
);
};

@ -0,0 +1,78 @@
import { Box, Button, useTheme } from "@mui/material";
import ArrowLeft from "@icons/questionsPage/arrowLeft";
import { FC } from "react";
type StepButtonsBlockProps = {
onSmallBtnClick?: () => void;
onLargeBtnClick?: () => void;
isSmallBtnMissing?: boolean;
isLargeBtnMissing?: boolean;
isSmallBtnDisabled?: boolean;
isLargeBtnDisabled?: boolean;
smallBtnText?: string;
largeBtnText?: string;
largeBtnType?: "button" | "submit" | "reset";
};
export const StepButtonsBlock: FC<StepButtonsBlockProps> = ({
onSmallBtnClick,
onLargeBtnClick,
isSmallBtnMissing,
isLargeBtnMissing,
smallBtnText,
largeBtnText,
isSmallBtnDisabled,
isLargeBtnDisabled,
largeBtnType,
}) => {
const theme = useTheme();
return (
<Box
sx={{
display: "flex",
gap: "10px",
marginTop: "auto",
marginLeft: "auto",
}}
>
{isSmallBtnMissing || (
<Button
variant="outlined"
sx={{
padding: "10px 20px",
borderRadius: "8px",
height: "44px",
color: theme.palette.brightPurple.main,
}}
data-cy="back-button"
disabled={isSmallBtnDisabled}
onClick={onSmallBtnClick}
>
{smallBtnText ? (
smallBtnText
) : (
<ArrowLeft color={theme.palette.brightPurple.main} />
)}
</Button>
)}
{isLargeBtnMissing || (
<Button
data-cy="next-step"
variant="contained"
disabled={isLargeBtnDisabled}
type={largeBtnType ? largeBtnType : "button"}
sx={{
height: "44px",
padding: "10px 20px",
borderRadius: "8px",
background: theme.palette.brightPurple.main,
fontSize: "18px",
}}
onClick={onLargeBtnClick}
>
{largeBtnText ? largeBtnText : "Далее"}
</Button>
)}
</Box>
);
};

@ -0,0 +1,77 @@
import { Skeleton, Typography, useMediaQuery, useTheme } from "@mui/material";
import React, { useEffect, useState } from "react";
import Box from "@mui/material/Box";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { useQuizStore } from "@root/quizes/store";
import { useNavigate } from "react-router-dom";
import { PartnersBoard } from "./PartnersBoard/PartnersBoard";
import { partnersMock } from "./mocks/MockData";
interface IntegrationsPageProps {
heightSidebar: number;
mobileSidebar: boolean;
}
export const IntegrationsPage = ({
heightSidebar,
mobileSidebar,
}: IntegrationsPageProps) => {
const quiz = useCurrentQuiz();
const { editQuizId } = useQuizStore();
const theme = useTheme();
const navigate = useNavigate();
const isMobile = useMediaQuery(theme.breakpoints.down(660));
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const [companyName, setCompanyName] = useState<string | null>(null);
useEffect(() => {
if (editQuizId === null) navigate("/list");
}, [navigate, editQuizId]);
const heightBar = heightSidebar + 51 + 88 + 36 + 25;
if (quiz === undefined)
return (
<Skeleton sx={{ width: "100vw", height: "100vh", transform: "none" }} />
);
const handleCloseModal = () => {
setIsModalOpen(false);
// setTimeout(() => {
// setCompanyName(null);
// }, 300);
};
return (
<>
<Box
sx={{
width: "100%",
padding: isMobile
? mobileSidebar
? `calc(${heightBar}px - 92px) 16px 70px 16px`
: "67px 16px 70px 16px"
: "25px",
height: isMobile ? "100vh" : "calc(100vh - 80px)",
}}
>
<Typography
variant="h5"
sx={{ marginBottom: "40px", color: "#333647" }}
>
Интеграции
</Typography>
<PartnersBoard
partners={partnersMock}
setIsModalOpen={setIsModalOpen}
setCompanyName={setCompanyName}
isModalOpen={isModalOpen}
handleCloseModal={handleCloseModal}
/>
{/*<IntegrationsModal*/}
{/* isModalOpen={isModalOpen}*/}
{/* handleCloseModal={handleCloseModal}*/}
{/* companyName={companyName}*/}
{/*/>*/}
</Box>
</>
);
};

@ -0,0 +1,50 @@
import { Box, Typography, useTheme } from "@mui/material";
import { FC } from "react";
import { Partner } from "../PartnersBoard";
type PartnerItemProps = {
partner: Partner;
setIsModalOpen: (value: boolean) => void;
setCompanyName: (value: string) => void;
};
export const PartnerItem: FC<PartnerItemProps> = ({
partner,
setIsModalOpen,
setCompanyName,
}) => {
const theme = useTheme();
const handleClick = () => {
setCompanyName(partner.name);
setIsModalOpen(true);
};
return (
<>
{partner && (
<Box
sx={{
width: 250,
height: 60,
backgroundColor: "white",
borderRadius: "8px",
padding: "0 20px",
display: "flex",
alignItems: "center",
marginBottom: "2%",
marginRight: "2%",
cursor: "pointer",
}}
onClick={handleClick}
>
{partner.logo ? (
<img height={"100%"} src={partner.logo} alt={partner.name} />
) : (
<Typography>{partner.name}</Typography>
)}
</Box>
)}
</>
);
};

@ -0,0 +1,94 @@
import { Box, Typography, useTheme } from "@mui/material";
import { FC } from "react";
import { YandexButton } from "../IntegrationYandex/YandexButton";
import YandexModal from "../IntegrationYandex/YandexModal";
export type Partner = {
name: string;
logo?: string;
category: string;
};
type PartnersBoardProps = {
partners: Partner[];
setIsModalOpen: (value: boolean) => void;
setCompanyName: (value: string) => void;
isModalOpen: boolean;
handleCloseModal: () => void;
};
export const PartnersBoard: FC<PartnersBoardProps> = ({
partners,
setIsModalOpen,
isModalOpen,
handleCloseModal,
setCompanyName,
}) => {
const theme = useTheme();
const partnersByCategory = partners.reduce(
(acc, partner) => {
(acc[partner.category] = acc[partner.category] || []).push(partner);
return acc;
},
{} as Record<string, Partner[]>,
);
return (
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
justifyContent: { xs: "center", sm: "center", md: "start" },
}}
>
{/*{Object.entries(partnersByCategory).map(([category, partners]) => (*/}
{/* <Box key={category}>*/}
{/* <Typography*/}
{/* variant="h6"*/}
{/* sx={{*/}
{/* textAlign: { xs: "center", sm: "start", md: "start" },*/}
{/* lineHeight: "1",*/}
{/* marginBottom: "12px",*/}
{/* }}*/}
{/* >*/}
{/* {category}*/}
{/* </Typography>*/}
{/* <Box*/}
{/* sx={{*/}
{/* display: "flex",*/}
{/* flexWrap: "wrap",*/}
{/* justifyContent: { xs: "center", sm: "start", md: "start" },*/}
{/* }}*/}
{/* >*/}
{/* {partners.map((partner) => (*/}
{/* <PartnerItem*/}
{/* key={partner.name}*/}
{/* partner={partner}*/}
{/* setIsModalOpen={setIsModalOpen}*/}
{/* setCompanyName={setCompanyName}*/}
{/* />*/}
{/* ))}*/}
{/* </Box>*/}
{/* </Box>*/}
{/*))}*/}
<Typography
variant="h6"
sx={{
textAlign: { xs: "center", sm: "start", md: "start" },
lineHeight: "1",
marginBottom: "12px",
}}
>
Аналитика
</Typography>
<YandexButton setIsModalOpen={setIsModalOpen} />
<YandexModal
isModalOpen={isModalOpen}
handleCloseModal={handleCloseModal}
/>
</Box>
);
};

@ -0,0 +1,33 @@
import amoCrmLogo from "./amoCrmLogo.png";
export const partnersMock = [
{ category: "CRM", name: "amoCRM", logo: amoCrmLogo },
];
export const performersMock = [
"Ангелина Полякова",
"Петр Иванов",
"Алексей Звягинцев",
"Никита Стрельцов",
"Инна Ким",
"Дмитрий Морозов",
"Арсен Тадевосян",
];
export const funnelsMock = [
"Воронка 1",
"Воронка 2",
"Воронка 3",
"Воронка 4",
"Воронка 5",
"Воронка 6",
];
export const stagesMock = [
"Этап 1",
"Этап 2",
"Этап 3",
"Этап 4",
"Этап 5",
"Этап 6",
];

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

@ -140,7 +140,7 @@ function TariffPage() {
link.href = `https://${isTestServer ? "s" : ""}hub.pena.digital/quizpayment?action=squizpay&dif=${cashDif}&data=${token}&userid=${userId}`;
document.body.appendChild(link);
link.click();
return
return;
}
//другая ошибка
enqueueSnackbar("Произошла ошибка. Попробуйте позже");
@ -171,18 +171,18 @@ function TariffPage() {
return tariff.privileges[0].privilegeId !== "squizHideBadge";
});
function handleApplyPromocode () {
function handleApplyPromocode() {
if (!promocodeField) return;
activatePromocode(promocodeField)
.then(async (greetings) => {
enqueueSnackbar(greetings)
enqueueSnackbar(greetings);
const discounts = await makeRequest({
method: "GET",
url: `${process.env.REACT_APP_DOMAIN}/price/discount/user/${userId}`,
});
setDiscounts(discounts.Discounts);
const discounts = await makeRequest({
method: "GET",
url: `${process.env.REACT_APP_DOMAIN}/price/discount/user/${userId}`,
});
setDiscounts(discounts.Discounts);
})
.catch(enqueueSnackbar);
}

@ -8,6 +8,7 @@ import { isAxiosError } from "axios";
import { enqueueSnackbar } from "notistack";
import { useParams } from "react-router-dom";
import useSWR from "swr";
import { useYandexMetrica } from "@utils/hooks/useYandexMetrica";
export default function ViewPublicationPage() {
const quizId = useParams().quizId;
@ -20,6 +21,9 @@ export default function ViewPublicationPage() {
if (!quizId) return null;
const quiz = quizes?.find((quiz) => quiz.qid === quizId);
const yandexMetricNumber = quiz?.config.yandexMetricNumber;
useYandexMetrica(yandexMetricNumber);
const {
data: rawQuestions,
@ -56,7 +60,6 @@ export default function ViewPublicationPage() {
if (!rawQuestions) throw new Error("Questions not found");
const questions = rawQuestions.map(rawQuestionToQuestion);
return (
<Box
sx={{

@ -1,8 +1,7 @@
import { Header } from "@ui_kit/Header/Header";
import Sidebar from "@ui_kit/Sidebar/Sidebar";
import Box from "@mui/material/Box";
import { useTheme, useMediaQuery, IconButton } from "@mui/material";
import HeaderFull from "@ui_kit/Header/HeaderFull";
import { IconButton, useMediaQuery, useTheme } from "@mui/material";
import { useCallback, useEffect, useRef, useState } from "react";
import { SidebarMobile } from "../ui_kit/Sidebar/SidebarMobile";
import { setShowConfirmLeaveModal } from "@root/uiTools/actions";
@ -16,7 +15,6 @@ import { Link } from "react-router-dom";
import { LinkSimple } from "@icons/LinkSimple";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { deleteTimeoutedQuestions } from "@utils/deleteTimeoutedQuestions";
import { useQuestionsStore } from "@root/questions/store";
import { quizApi } from "@api/quiz";
import { questionApi } from "@api/question";
import { createResult, setQuestions } from "@root/questions/actions";
@ -38,7 +36,6 @@ export default function Main({ sidebar, header, footer, Page }: Props) {
const { editQuizId } = useQuizStore();
const currentStep = useQuizStore((state) => state.currentStep);
const { isTestServer } = useDomainDefine();
useEffect(() => {
const getData = async () => {
const quizes = await quizApi.getList();

@ -1,15 +1,14 @@
import type { ChangeEvent, FocusEvent, KeyboardEvent } from "react";
import React, { useEffect, useState } from "react";
import type { InputProps, SxProps, Theme } from "@mui/material";
import {
Box,
FormControl,
TextField,
Typography,
useTheme,
Input,
InputLabel,
Typography,
useTheme,
} from "@mui/material";
import type { ChangeEvent, KeyboardEvent, FocusEvent } from "react";
import type { InputProps, SxProps, Theme } from "@mui/material";
interface CustomTextFieldProps {
placeholder: string;
@ -28,6 +27,7 @@ interface CustomTextFieldProps {
type?: string;
rows?: number;
className?: string;
disabled?: boolean;
}
export default function CustomTextField({
@ -47,6 +47,7 @@ export default function CustomTextField({
rows = 0,
sxForm,
className,
disabled,
}: CustomTextFieldProps) {
const theme = useTheme();
@ -115,6 +116,7 @@ export default function CustomTextField({
onKeyDown={onKeyDown}
multiline={rows > 0}
rows={rows}
disabled={disabled}
disableUnderline
sx={{
maxLength: maxLength,

@ -178,6 +178,35 @@ export default function Sidebar({ changePage, disableCollapse }: SidebarProps) {
/>
}
/>
<MenuItem
onClick={() => {
navigate("/integrations");
setCurrentStep(16);
}}
text={"Интеграции"}
isCollapsed={isMenuCollapsed}
isActive={pathname.startsWith("/integrations")}
disabled={
pathname.startsWith("/integrations")
? false
: quiz === undefined
? true
: quiz?.config.type === null
}
icon={
<PuzzlePieceIcon
color={
pathname.startsWith("/integrations")
? theme.palette.brightPurple.main
: isMenuCollapsed
? "white"
: theme.palette.grey2.main
}
height={isMenuCollapsed ? "35px" : "24px"}
width={isMenuCollapsed ? "35px" : "24px"}
/>
}
/>
{/* {quizSettingsMenuItems.map((menuItem, index) => {
const Icon = menuItem[0];

@ -1,4 +1,4 @@
import React, { FC, useEffect, useRef, useState } from "react";
import React, { ChangeEvent, FC, useEffect, useRef, useState } from "react";
import {
Box,
FormControl,
@ -49,7 +49,7 @@ export const SidebarMobile: FC<Iprops> = ({
const [inputOpen, setInputOpen] = useState<boolean>(false);
const quiz = useCurrentQuiz();
const [inputValue, setInputValue] = useState(quiz.name);
const ref = useRef(null);
const ref = useRef<HTMLInputElement | null>(null);
const heightSidebar = useRef(null);
const navigate = useNavigate();
const { pathname } = useLocation();
@ -62,15 +62,19 @@ export const SidebarMobile: FC<Iprops> = ({
);
useEffect(() => {
observer.current.observe(heightSidebar.current);
if (heightSidebar.current) {
observer.current.observe(heightSidebar.current);
}
}, [heightSidebar, observer]);
const handleClick = (event) => {
const handleClick = (event: ChangeEvent<HTMLDivElement>) => {
setAnchorEl(anchorEl ? null : event.currentTarget);
};
const clickInput = (event) => {
if (ref.current && !ref.current.contains(event.target)) setInputOpen(false);
const clickInput = (event: MouseEvent) => {
debugger;
if (ref.current && !ref.current?.contains(event.target as Node))
setInputOpen(false);
};
useEffect(() => {
document.addEventListener("mousedown", clickInput);
@ -88,7 +92,7 @@ export const SidebarMobile: FC<Iprops> = ({
changePage(index);
};
const openPopper = Boolean(anchorEl);
const id = openPopper ? "simple-popper" : undefined;
const id = openPopper ? "simple-popper" : "";
return (
<Box
ref={heightSidebar}

@ -6,7 +6,7 @@ type SidebarModalProps = {
open: boolean;
handleClick: () => void;
changePage: (step: number) => void;
anchorEl: HTMLElement;
anchorEl: HTMLElement | null;
id: string;
};
export const SidebarModal = ({

@ -10,7 +10,7 @@ import {
import { Quiz } from "@model/quiz/quiz";
export const checkQuestionHint = (
questions: AnyTypedQuizQuestion,
questions: AnyTypedQuizQuestion[],
quiz: Quiz,
): Record<string, WhyCantCreatePublic> => {
const problems: any = {};
@ -77,12 +77,17 @@ export const checkQuestionHint = (
(condition: QuestionBranchingRuleMain) => {
buffer.forEach((oldCondition: QuestionBranchingRuleMain) => {
if (areRulesEqual(condition.rules, oldCondition.rules)) {
const q = getQuestionByContentId(condition.next);
const oldq = getQuestionByContentId(oldCondition.next);
const currentQuestion = getQuestionByContentId(condition.next);
const oldQuestions = getQuestionByContentId(oldCondition.next);
if (!currentQuestion?.type || !oldQuestions?.type) {
return;
}
pushProblem(
question.content.id,
`У вопроса "${q?.title || "noname №" + q?.page}" и "${
oldq?.title || "noname №" + oldq?.page
`У вопроса "${currentQuestion.title || "noname №" + currentQuestion.page}" и "${
oldQuestions.title || "noname №" + oldQuestions.page
}" одинаковые условия ветвления`,
question.title,
);

@ -1,4 +1,3 @@
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import {
clearRuleForAll,
createResult,
@ -10,6 +9,13 @@ import { useQuestionsStore } from "@root/questions/store";
import { updateRootContentId } from "@root/quizes/actions";
import { getCurrentQuiz } from "@root/quizes/hooks";
import type {
AnyTypedQuizQuestion,
QuestionBranchingRule,
QuestionBranchingRuleMain,
} from "@model/questionTypes/shared";
import { QuizQuestionResult } from "@model/questionTypes/result";
//Всё здесь нужно сделать последовательно. И пусть весь мир ждёт.
export const DeleteFunction = async (questionId: string) => {
@ -33,7 +39,9 @@ export const DeleteFunction = async (questionId: string) => {
const parentQuestion = getQuestionByContentId(
question.content.rule.parentId,
);
let startCountParentChildren = parentQuestion.content.rule.children;
let startCountParentChildren = parentQuestion?.type
? parentQuestion.content.rule.children
: null;
//записываем потомков , а их результаты удаляем
const getChildren = (parentQuestion: AnyTypedQuizQuestion) => {
@ -67,8 +75,10 @@ export const DeleteFunction = async (questionId: string) => {
}),
);
//чистим rule родителя
const newRule = {};
if (!parentQuestion?.type) {
return;
}
const parentChildren = [...parentQuestion.content.rule.children];
if (parentChildren.includes(question.content.id))
@ -77,15 +87,21 @@ export const DeleteFunction = async (questionId: string) => {
1,
);
newRule.main = parentQuestion.content.rule.main.filter(
(data) => data.next !== question.content.id,
const main = parentQuestion.content.rule.main.filter(
(data: QuestionBranchingRuleMain) => data.next !== question.content.id,
); //удаляем условия перехода от родителя к этому вопросу
newRule.parentId = parentQuestion.content.rule.parentId;
newRule.default =
const defaultValue =
parentQuestion.content.rule.parentId === question.content.id
? ""
: parentQuestion.content.rule.parentId;
newRule.children = parentChildren;
//чистим rule родителя
const newRule: QuestionBranchingRule = {
main,
default: defaultValue,
children: parentChildren,
parentId: parentQuestion.content.rule.parentId,
};
await updateQuestion(question.content.rule.parentId, (PQ) => {
PQ.content.rule = newRule;
@ -101,10 +117,13 @@ export const DeleteFunction = async (questionId: string) => {
//сделать результ родителя видимым если у него не осталось потомков
if (startCountParentChildren.length === 1) {
if (parentResult) {
await updateQuestion(parentResult.content.id, (q) => {
q.content.usage = true;
});
if (parentResult?.type) {
await updateQuestion<QuizQuestionResult>(
parentResult.content.id,
(item) => {
item.content.usage = true;
},
);
} else {
//почему-то не существует результа у родителя. Создаём. Новосозданные результы видны сразу
await createResult(quiz.backendId, parentQuestion.content.id);

@ -0,0 +1,29 @@
import { useEffect } from "react";
export const useYandexMetrica = (yandexMetricNumber: number | undefined) => {
useEffect(() => {
if (yandexMetricNumber) {
const script = document.createElement("script");
script.type = "text/javascript";
script.innerHTML = `
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
ym(${yandexMetricNumber}, "init", {
clickmap:true,
trackLinks:true,
accurateTrackBounce:true,
webvisor:true
});
`;
document.body.appendChild(script);
const noscript = document.createElement("noscript");
noscript.innerHTML = `<div><img src="https://mc.yandex.ru/watch/${yandexMetricNumber}" style="position:absolute; left:-9999px;" alt="" /></div>`;
document.body.appendChild(noscript);
}
}, [yandexMetricNumber]);
};