xMerge branch 'staging' into 'main'
refactored YandexModal, added yandexMetricNumber to quiz config, added logic... See merge request frontend/squiz!282
@ -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>
|
||||
|
11
src/App.tsx
@ -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;
|
||||
|
52
src/assets/icons/AccountSetting.tsx
Normal file
@ -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",
|
||||
|
36
src/assets/icons/EditPencil.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
13
src/assets/icons/arrow_down.svg
Normal file
@ -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 |
BIN
src/assets/icons/designs/smallSize/design1.jpg
Normal file
After Width: | Height: | Size: 591 KiB |
BIN
src/assets/icons/designs/smallSize/design10.jpg
Normal file
After Width: | Height: | Size: 433 KiB |
BIN
src/assets/icons/designs/smallSize/design2.jpg
Normal file
After Width: | Height: | Size: 309 KiB |
BIN
src/assets/icons/designs/smallSize/design3.jpg
Normal file
After Width: | Height: | Size: 210 KiB |
BIN
src/assets/icons/designs/smallSize/design4.jpg
Normal file
After Width: | Height: | Size: 281 KiB |
BIN
src/assets/icons/designs/smallSize/design5.jpg
Normal file
After Width: | Height: | Size: 290 KiB |
BIN
src/assets/icons/designs/smallSize/design6.jpg
Normal file
After Width: | Height: | Size: 430 KiB |
BIN
src/assets/icons/designs/smallSize/design7.jpg
Normal file
After Width: | Height: | Size: 352 KiB |
BIN
src/assets/icons/designs/smallSize/design8.jpg
Normal file
After Width: | Height: | Size: 299 KiB |
BIN
src/assets/icons/designs/smallSize/design9.jpg
Normal file
After Width: | Height: | Size: 409 KiB |
81
src/components/CustomRadioGroup/CustomRadioGroup.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
22
src/components/CustomSelect/CustomSelect.css
Normal file
@ -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;
|
||||
}
|
129
src/components/CustomSelect/CustomSelect.tsx
Normal file
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
186
src/pages/IntegrationsPage/IntegrationYandex/YandexModal.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
94
src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep5/CustomFileUploader/CustomFileUploader.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
58
src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep5/FileBlock/FileBlock.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
39
src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/Item/AnswerItem/AnswerItem.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
46
src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/Item/IconBtnAdd/IconBtnAdd.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
78
src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/ItemDetailsView/ItemDetailsView.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
60
src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/ItemsSelectionView/ItemsSelectionView.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
99
src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep7/TagsDetailsView/TagsDetailsView.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
120
src/pages/IntegrationsPage/IntegrationsModal/IntegrationsModalTitle/IntegrationsModalTitle.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
38
src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingItem/ResponsiblePerson/ResponsiblePerson.tsx
Normal file
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
27
src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingItem/SelectedParameter/SelectedParameter.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
173
src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingItem/SettingItem.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
64
src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingItem/SettingItemHeader/SettingItemHeader.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
77
src/pages/IntegrationsPage/IntegrationsPage.tsx
Normal file
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
94
src/pages/IntegrationsPage/PartnersBoard/PartnersBoard.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
33
src/pages/IntegrationsPage/mocks/MockData.ts
Normal file
@ -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",
|
||||
];
|
BIN
src/pages/IntegrationsPage/mocks/YandexMetric.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/pages/IntegrationsPage/mocks/amoCrmLogo.png
Normal file
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);
|
||||
|
29
src/utils/hooks/useYandexMetrica.tsx
Normal file
@ -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]);
|
||||
};
|