diff --git a/public/index.html b/public/index.html index a355adcd..36dd979f 100755 --- a/public/index.html +++ b/public/index.html @@ -39,27 +39,23 @@ - - - - + ym(96979576, "init", { + clickmap:true, + trackLinks:true, + accurateTrackBounce:true, + webvisor:true + }); + + + @@ -67,4 +63,4 @@
- \ No newline at end of file + diff --git a/src/App.tsx b/src/App.tsx index 2955220b..9b451632 100644 --- a/src/App.tsx +++ b/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) => ( diff --git a/src/api/makeRequest.ts b/src/api/makeRequest.ts index f3fd8dac..a3bdef6c 100644 --- a/src/api/makeRequest.ts +++ b/src/api/makeRequest.ts @@ -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( + data: MakeRequest, +): Promise { + try { + const response = await KIT.makeRequest(data); -async function makeRequest(data: MakeRequest): Promise { - try { - const response = await KIT.makeRequest(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; \ No newline at end of file + 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; diff --git a/src/assets/icons/AccountSetting.tsx b/src/assets/icons/AccountSetting.tsx new file mode 100644 index 00000000..7fd40089 --- /dev/null +++ b/src/assets/icons/AccountSetting.tsx @@ -0,0 +1,52 @@ +import { Box } from "@mui/material"; + +interface Props { + color: string; + height: string; + width: string; +} + +export default function AccountSetting({ color, height, width }: Props) { + return ( + + + + + + + + + ); +} diff --git a/src/assets/icons/Checkbox.tsx b/src/assets/icons/Checkbox.tsx index 1225b1aa..a569d8b2 100644 --- a/src/assets/icons/Checkbox.tsx +++ b/src/assets/icons/Checkbox.tsx @@ -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", diff --git a/src/assets/icons/EditPencil.tsx b/src/assets/icons/EditPencil.tsx new file mode 100644 index 00000000..f9521cac --- /dev/null +++ b/src/assets/icons/EditPencil.tsx @@ -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 ( + + + + + + ); +} diff --git a/src/assets/icons/arrow_down.svg b/src/assets/icons/arrow_down.svg new file mode 100644 index 00000000..1ad046ea --- /dev/null +++ b/src/assets/icons/arrow_down.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/assets/icons/designs/smallSize/design1.jpg b/src/assets/icons/designs/smallSize/design1.jpg new file mode 100644 index 00000000..d3886588 Binary files /dev/null and b/src/assets/icons/designs/smallSize/design1.jpg differ diff --git a/src/assets/icons/designs/smallSize/design10.jpg b/src/assets/icons/designs/smallSize/design10.jpg new file mode 100644 index 00000000..ce2e3b7b Binary files /dev/null and b/src/assets/icons/designs/smallSize/design10.jpg differ diff --git a/src/assets/icons/designs/smallSize/design2.jpg b/src/assets/icons/designs/smallSize/design2.jpg new file mode 100644 index 00000000..fa586fa2 Binary files /dev/null and b/src/assets/icons/designs/smallSize/design2.jpg differ diff --git a/src/assets/icons/designs/smallSize/design3.jpg b/src/assets/icons/designs/smallSize/design3.jpg new file mode 100644 index 00000000..a3802a6a Binary files /dev/null and b/src/assets/icons/designs/smallSize/design3.jpg differ diff --git a/src/assets/icons/designs/smallSize/design4.jpg b/src/assets/icons/designs/smallSize/design4.jpg new file mode 100644 index 00000000..c1a0b287 Binary files /dev/null and b/src/assets/icons/designs/smallSize/design4.jpg differ diff --git a/src/assets/icons/designs/smallSize/design5.jpg b/src/assets/icons/designs/smallSize/design5.jpg new file mode 100644 index 00000000..e1353864 Binary files /dev/null and b/src/assets/icons/designs/smallSize/design5.jpg differ diff --git a/src/assets/icons/designs/smallSize/design6.jpg b/src/assets/icons/designs/smallSize/design6.jpg new file mode 100644 index 00000000..83b7f27f Binary files /dev/null and b/src/assets/icons/designs/smallSize/design6.jpg differ diff --git a/src/assets/icons/designs/smallSize/design7.jpg b/src/assets/icons/designs/smallSize/design7.jpg new file mode 100644 index 00000000..2b819452 Binary files /dev/null and b/src/assets/icons/designs/smallSize/design7.jpg differ diff --git a/src/assets/icons/designs/smallSize/design8.jpg b/src/assets/icons/designs/smallSize/design8.jpg new file mode 100644 index 00000000..262c94cc Binary files /dev/null and b/src/assets/icons/designs/smallSize/design8.jpg differ diff --git a/src/assets/icons/designs/smallSize/design9.jpg b/src/assets/icons/designs/smallSize/design9.jpg new file mode 100644 index 00000000..97b8492c Binary files /dev/null and b/src/assets/icons/designs/smallSize/design9.jpg differ diff --git a/src/components/CustomRadioGroup/CustomRadioGroup.tsx b/src/components/CustomRadioGroup/CustomRadioGroup.tsx new file mode 100644 index 00000000..3710f25d --- /dev/null +++ b/src/components/CustomRadioGroup/CustomRadioGroup.tsx @@ -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 = ({ + items, + selectedValue, + setSelectedValue, +}) => { + const theme = useTheme(); + const [currentValue, setCurrentValue] = React.useState( + selectedValue, + ); + const handleChange = (event: React.ChangeEvent) => { + setSelectedValue((event.target as HTMLInputElement).value); + setCurrentValue((event.target as HTMLInputElement).value); + }; + return ( + + + {items.map((item) => ( + + } + icon={} + /> + } + label={item} + labelPlacement={"start"} + /> + ))} + + + ); +}; diff --git a/src/components/CustomSelect/CustomSelect.css b/src/components/CustomSelect/CustomSelect.css new file mode 100644 index 00000000..e16e64cc --- /dev/null +++ b/src/components/CustomSelect/CustomSelect.css @@ -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; +} diff --git a/src/components/CustomSelect/CustomSelect.tsx b/src/components/CustomSelect/CustomSelect.tsx new file mode 100644 index 00000000..c98c5d55 --- /dev/null +++ b/src/components/CustomSelect/CustomSelect.tsx @@ -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 = ({ + items, + selectedItem, + setSelectedItem, +}) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(600)); + const [opened, setOpened] = useState(false); + const [currentValue, setCurrentValue] = useState(selectedItem); + const ref = useRef(null); + + const onSelectItem = useCallback( + (event: SelectChangeEvent) => { + const newValue = event.target.value.toString(); + setCurrentValue(newValue); + setSelectedItem(newValue); + }, + [setSelectedItem, setCurrentValue], + ); + + const toggleOpened = useCallback(() => { + setOpened((isOpened) => !isOpened); + }, []); + + return ( + + ref.current?.click()} + > + + + {currentValue || "Выберите ответственного за сделку"} + + check + + + + ); +}; diff --git a/src/model/quizSettings.ts b/src/model/quizSettings.ts index 6f1cdced..d055a121 100644 --- a/src/model/quizSettings.ts +++ b/src/model/quizSettings.ts @@ -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, }; diff --git a/src/pages/Analytics/Analytics.tsx b/src/pages/Analytics/Analytics.tsx index 87a7635c..de409bfb 100644 --- a/src/pages/Analytics/Analytics.tsx +++ b/src/pages/Analytics/Analytics.tsx @@ -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": { diff --git a/src/pages/Analytics/General.tsx b/src/pages/Analytics/General.tsx index d8bf3ebf..1ba30350 100644 --- a/src/pages/Analytics/General.tsx +++ b/src/pages/Analytics/General.tsx @@ -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 ( {`${title} - нет данных`} ); } + return ( {title} - {calculateTime - ? `${getCalculatedTime(numberValue ?? 0)} с` - : `${numberValue?.toFixed(2) ?? 0}%`} + {calculateTime ? `${getCalculatedTime(numberValue)} с` : `${numberValue.toFixed(2)}%`} - 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 = ({ data, day }) => { const theme = useTheme(); const isTablet = useMediaQuery(theme.breakpoints.down(1000)); diff --git a/src/pages/DesignPage/DesignFilling.tsx b/src/pages/DesignPage/DesignFilling.tsx index df0c1173..ade55b80 100644 --- a/src/pages/DesignPage/DesignFilling.tsx +++ b/src/pages/DesignPage/DesignFilling.tsx @@ -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[] = [ { diff --git a/src/pages/InstallQuiz/InstallQuiz.tsx b/src/pages/InstallQuiz/InstallQuiz.tsx index d63f3257..47f8268f 100644 --- a/src/pages/InstallQuiz/InstallQuiz.tsx +++ b/src/pages/InstallQuiz/InstallQuiz.tsx @@ -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={`
`} + value={`
`} sx={{ "& .MuiInputBase-root": { maxWidth: "520px", diff --git a/src/pages/IntegrationsPage/IntegrationYandex/YandexButton.tsx b/src/pages/IntegrationsPage/IntegrationYandex/YandexButton.tsx new file mode 100644 index 00000000..64e6e843 --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationYandex/YandexButton.tsx @@ -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 = ({ + setIsModalOpen, + setCompanyName, +}) => { + const theme = useTheme(); + + const handleClick = () => { + setIsModalOpen(true); + }; + + return ( + <> + setIsModalOpen(true)} + > + {"Yandex.Метрика"} + + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationYandex/YandexModal.tsx b/src/pages/IntegrationsPage/IntegrationYandex/YandexModal.tsx new file mode 100644 index 00000000..c2543032 --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationYandex/YandexModal.tsx @@ -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(!!yandexNumber); + const [currentValue, setCurrentValue] = useState( + 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 ( + + + + Аналитика с Яндекс.Метрикой + + + + + + + + + + {isSave ? "Ваш номер счетчика" : "Введите номер счетчика"} + + {isSave && ( + + + + + + + + + )} + + + { + const onlyNums = e.target.value.replace(/[^0-9]/g, ""); + setCurrentValue(onlyNums); + }} + /> + + {!isSave && ( + + + + + )} + + + ); +} diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep1/IntegrationStep1.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep1/IntegrationStep1.tsx new file mode 100644 index 00000000..6074d652 --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep1/IntegrationStep1.tsx @@ -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 = ({ + handleNextStep, +}) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(600)); + + const formik = useFormik({ + 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 ( + + + + + + + + Инструкция + + + Повседневная практика показывает, что постоянный количественный рост и + сфера нашей активности способствует подготовки и реализации систем + массового участия + + + + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep2/IntegrationStep2.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep2/IntegrationStep2.tsx new file mode 100644 index 00000000..edd16fd2 --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep2/IntegrationStep2.tsx @@ -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 = ({ + 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 ( + + + + + + + + + + + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep3/IntegrationStep3.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep3/IntegrationStep3.tsx new file mode 100644 index 00000000..75d77f10 --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep3/IntegrationStep3.tsx @@ -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 = ({ + 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 ( + + + + + + + + + + + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep4/IntegrationStep4.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep4/IntegrationStep4.tsx new file mode 100644 index 00000000..88009b2e --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep4/IntegrationStep4.tsx @@ -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 = ({ + handlePrevStep, + handleNextStep, + selectedDealPerformer, + setSelectedDealPerformer, + performers, +}) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(600)); + const isTablet = useMediaQuery(theme.breakpoints.down(1000)); + + return ( + + + + + + + + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep5/CustomFileUploader/CustomFileUploader.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep5/CustomFileUploader/CustomFileUploader.tsx new file mode 100644 index 00000000..8d055829 --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep5/CustomFileUploader/CustomFileUploader.tsx @@ -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 = ({ + accept, + description, + handleImageChange, +}) => { + const theme = useTheme(); + const dropZone = useRef(null); + const [ready, setReady] = useState(false); + const isMobile = useMediaQuery(theme.breakpoints.down(700)); + + const handleDragEnter = (event: DragEvent) => { + event.preventDefault(); + setReady(true); + }; + + const handleDrop = (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + + const file = event.dataTransfer.files[0]; + if (!file) return; + + handleImageChange(file); + }; + + const acceptedFormats = accept + ? accept.map((format) => "." + format).join(", ") + : ""; + + return ( + + { + 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" + /> + ) => + 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} + > + + + + Добавить файл + + + {description || "Принимает JPG, PNG, и GIF формат — максимум 5mb"} + + + + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep5/FileBlock/FileBlock.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep5/FileBlock/FileBlock.tsx new file mode 100644 index 00000000..89d63fcf --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep5/FileBlock/FileBlock.tsx @@ -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 = ({ setFile, file }) => { + const theme = useTheme(); + return ( + + + Вы загрузили: + + + + {file?.name} + + {setFile && ( + setFile(null)} + sx={{ + backgroundColor: "#864BD9", + borderRadius: "50%", + width: "24px", + height: "24px", + color: "white", + }} + > + + + )} + + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep5/IntegrationStep5.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep5/IntegrationStep5.tsx new file mode 100644 index 00000000..dd5a60c1 --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep5/IntegrationStep5.tsx @@ -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 = ({ + handlePrevStep, + handleNextStep, + utmFile, + setUtmFile, +}) => { + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + + return ( + + + {utmFile ? ( + + ) : ( + + )} + + + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/IntegrationStep6.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/IntegrationStep6.tsx new file mode 100644 index 00000000..fb7f7a1b --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/IntegrationStep6.tsx @@ -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>; +}; + +export const IntegrationStep6: FC = ({ + handlePrevStep, + handleNextStep, + questionEntity, + setQuestionEntity, +}) => { + const theme = useTheme(); + const [isSelection, setIsSelection] = useState(false); + const [activeItem, setActiveItem] = useState(null); + const [selectedValue, setSelectedValue] = useState(null); + + const handleAdd = useCallback(() => { + if (!activeItem || !selectedValue) return; + + setQuestionEntity((prevState) => ({ + ...prevState, + [activeItem]: [...prevState[activeItem as TitleKeys], selectedValue], + })); + }, [activeItem, setQuestionEntity, selectedValue]); + + const items = useMemo( + () => ["Город", "Имя", "Фамилия", "Отчество", "Контрагент"], + [], + ); + + return ( + + {isSelection ? ( + { + setActiveItem(null); + setIsSelection(false); + }} + onLargeBtnClick={() => { + handleAdd(); + setActiveItem(null); + setIsSelection(false); + }} + /> + ) : ( + + )} + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/Item/AnswerItem/AnswerItem.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/Item/AnswerItem/AnswerItem.tsx new file mode 100644 index 00000000..f4082034 --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/Item/AnswerItem/AnswerItem.tsx @@ -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 = ({ fieldName, fieldValue }) => { + const theme = useTheme(); + return ( + + + {fieldName} + + + {fieldValue} + + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/Item/IconBtnAdd/IconBtnAdd.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/Item/IconBtnAdd/IconBtnAdd.tsx new file mode 100644 index 00000000..e33e54ee --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/Item/IconBtnAdd/IconBtnAdd.tsx @@ -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 = ({ onAddBtnClick }) => { + const theme = useTheme(); + + return ( + + + + + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/Item/Item.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/Item/Item.tsx new file mode 100644 index 00000000..55c746f9 --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/Item/Item.tsx @@ -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 = ({ title, onAddBtnClick, data }) => { + const theme = useTheme(); + + const titleDictionary = { + contact: "Контакт", + company: "Компания", + deal: "Сделка", + buyer: "Покупатель", + contacts: "Контакты", + users: "Пользователи", + buyers: "Покупатели", + }; + + const translatedTitle = titleDictionary[title]; + const selectedOptions = data[title]; + return ( + + + + {translatedTitle} + + + {selectedOptions && + selectedOptions.map((text, index) => ( + + ))} + + + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/ItemDetailsView/ItemDetailsView.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/ItemDetailsView/ItemDetailsView.tsx new file mode 100644 index 00000000..a22eec6e --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/ItemDetailsView/ItemDetailsView.tsx @@ -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 = ({ + handlePrevStep, + handleNextStep, + questionEntity, + setActiveItem, + setIsSelection, +}) => { + const theme = useTheme(); + + return ( + + + {questionEntity && + Object.keys(questionEntity).map((item) => ( + { + setIsSelection(true); + setActiveItem(item); + }} + data={questionEntity} + /> + ))} + + + + + + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/ItemsSelectionView/ItemsSelectionView.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/ItemsSelectionView/ItemsSelectionView.tsx new file mode 100644 index 00000000..3523352f --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/ItemsSelectionView/ItemsSelectionView.tsx @@ -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 = ({ + items, + selectedValue, + setSelectedValue, + onLargeBtnClick, + onSmallBtnClick, +}) => { + return ( + + + + + + + + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep7/IntegrationStep7.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep7/IntegrationStep7.tsx new file mode 100644 index 00000000..80607d8b --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep7/IntegrationStep7.tsx @@ -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>; +}; + +export const IntegrationStep7: FC = ({ + handleSmallBtn, + handleLargeBtn, + tags, + setTags, +}) => { + const theme = useTheme(); + const [isSelection, setIsSelection] = useState(false); + const [activeItem, setActiveItem] = useState(null); + const [selectedValue, setSelectedValue] = useState(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 ( + + {isSelection ? ( + { + setActiveItem(null); + setIsSelection(false); + }} + onLargeBtnClick={() => { + handleAdd(); + setActiveItem(null); + setIsSelection(false); + }} + /> + ) : ( + + )} + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep7/TagsDetailsView/TagsDetailsView.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep7/TagsDetailsView/TagsDetailsView.tsx new file mode 100644 index 00000000..43b050f7 --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep7/TagsDetailsView/TagsDetailsView.tsx @@ -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 = ({ + handleSmallBtn, + handleLargeBtn, + tags, + setActiveItem, + setIsSelection, +}) => { + const theme = useTheme(); + + return ( + + + + + Результат + + + + {tags && + Object.keys(tags).map((item) => ( + { + setIsSelection(true); + setActiveItem(item); + }} + data={tags} + /> + ))} + + + + + + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationsModal.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationsModal.tsx new file mode 100644 index 00000000..e78f1d71 --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationsModal.tsx @@ -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; +type IntegrationsModalProps = { + isModalOpen: boolean; + handleCloseModal: () => void; + companyName: string | null; +}; + +export type TagKeys = "contact" | "company" | "deal" | "buyer"; +export type TTags = Record; + +export const IntegrationsModal: FC = ({ + isModalOpen, + handleCloseModal, + companyName, +}) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(600)); + const isTablet = useMediaQuery(theme.breakpoints.down(1000)); + + const [step, setStep] = useState(0); + const [isSettingsBlock, setIsSettingsBlock] = useState(false); + const [selectedFunnelPerformer, setSelectedFunnelPerformer] = useState< + string | null + >(null); + const [selectedFunnel, setSelectedFunnel] = useState(null); + const [selectedStagePerformer, setSelectedStagePerformer] = useState< + string | null + >(null); + const [selectedStage, setSelectedStage] = useState(null); + const [selectedDealPerformer, setSelectedDealPerformer] = useState< + string | null + >(null); + const [utmFile, setUtmFile] = useState(null); + const [questionEntity, setQuestionEntity] = useState({ + contacts: [], + company: [], + deal: [], + users: [], + buyers: [], + }); + const [tags, setTags] = useState({ + 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: , + }, + { + title: "Выбор воронки", + isSettingsAvailable: true, + component: ( + + ), + }, + { + title: "Выбор этапа воронки", + isSettingsAvailable: true, + component: ( + + ), + }, + { + title: "Сделка", + isSettingsAvailable: true, + component: ( + + ), + }, + { + title: "Добавление utm-меток", + isSettingsAvailable: false, + component: ( + + ), + }, + { + title: "Соотнесение вопросов и сущностей", + isSettingsAvailable: true, + component: ( + + ), + }, + { + title: "Добавление тегов", + isSettingsAvailable: true, + component: ( + + ), + }, + ], + [ + questionEntity, + utmFile, + selectedFunnelPerformer, + selectedFunnel, + selectedStagePerformer, + selectedStage, + selectedDealPerformer, + tags, + ], + ); + + const stepTitles = steps.map((step) => step.title); + + return ( + + + + Интеграция с {companyName ? companyName : "партнером"} + + + + + + + + {isSettingsBlock ? ( + + + + ) : ( + {steps[step].component} + )} + + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationsModalTitle/IntegrationsModalTitle.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationsModalTitle/IntegrationsModalTitle.tsx new file mode 100644 index 00000000..93462345 --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationsModalTitle/IntegrationsModalTitle.tsx @@ -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 = ({ + 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 ( + + + + {isSettingsBlock ? "Мои настройки" : steps[step].title} + + {isSettingsBlock || ( + + Шаг {step + 1} + + )} + + {steps[step].isSettingsAvailable && ( + + )} + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingItem/ResponsiblePerson/ResponsiblePerson.tsx b/src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingItem/ResponsiblePerson/ResponsiblePerson.tsx new file mode 100644 index 00000000..8d02911c --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingItem/ResponsiblePerson/ResponsiblePerson.tsx @@ -0,0 +1,38 @@ +import { Typography, useTheme } from "@mui/material"; +import { FC } from "react"; + +type ResponsiblePersonProps = { + performer: string | null; +}; + +export const ResponsiblePerson: FC = ({ + performer, +}) => { + const theme = useTheme(); + + return ( + <> + + Ответственный за сделку: + + + {performer ? performer : "Не выбран"} + + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingItem/SelectedParameter/SelectedParameter.tsx b/src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingItem/SelectedParameter/SelectedParameter.tsx new file mode 100644 index 00000000..f4d068bb --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingItem/SelectedParameter/SelectedParameter.tsx @@ -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 = ({ + parameter, +}) => { + const theme = useTheme(); + return ( + + {parameter ? parameter : "Не выбрано"} + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingItem/SettingItem.tsx b/src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingItem/SettingItem.tsx new file mode 100644 index 00000000..80ace601 --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingItem/SettingItem.tsx @@ -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 = ({ + 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 ( + <> + + + + ); + } + if (step === 2) { + return ( + <> + + + + ); + } + if (step === 3) { + return ( + <> + + + ); + } + if (step === 4) { + return ( + + {utmFile ? ( + + ) : ( + Файл не загружен + )} + + ); + } + if (step === 5) { + const isFilled = Object.values(questionEntity).some( + (array) => array.length > 0, + ); + const status = isFilled ? "Заполнено" : "Не заполнено"; + + return ( + <> + + Статус: + + + {status} + + + ); + } + if (step === 6) { + const isFilled = Object.values(tags).some((array) => array.length > 0); + const status = isFilled ? "Заполнено" : "Не заполнено"; + + return ( + <> + + Статус: + + + {status} + + + ); + } + return null; + }, [ + step, + selectedFunnelPerformer, + selectedFunnel, + selectedStagePerformer, + selectedDealPerformer, + selectedStage, + utmFile, + questionEntity, + tags, + ]); + + return ( + + + {SettingsContent} + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingItem/SettingItemHeader/SettingItemHeader.tsx b/src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingItem/SettingItemHeader/SettingItemHeader.tsx new file mode 100644 index 00000000..61a43ea2 --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingItem/SettingItemHeader/SettingItemHeader.tsx @@ -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 = ({ + title, + step, + setStep, + setIsSettingsBlock, +}) => { + const theme = useTheme(); + + const handleClick = () => { + setStep(step); + setIsSettingsBlock(false); + }; + + return ( + + + + {step} этап + + + + + + + {title} + + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingsBlock.tsx b/src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingsBlock.tsx new file mode 100644 index 00000000..523666fd --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingsBlock.tsx @@ -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 = ({ + stepTitles, + setStep, + setIsSettingsBlock, + selectedFunnelPerformer, + selectedFunnel, + selectedStagePerformer, + selectedDealPerformer, + selectedStage, + utmFile, + questionEntity, + tags, +}) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(600)); + + return ( + + + {stepTitles && + stepTitles.map((title, index) => ( + + ))} + + + setIsSettingsBlock(false)} + isLargeBtnMissing={true} + /> + + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/StepButtonsBlock/StepButtonsBlock.tsx b/src/pages/IntegrationsPage/IntegrationsModal/StepButtonsBlock/StepButtonsBlock.tsx new file mode 100644 index 00000000..d829f09a --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/StepButtonsBlock/StepButtonsBlock.tsx @@ -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 = ({ + onSmallBtnClick, + onLargeBtnClick, + isSmallBtnMissing, + isLargeBtnMissing, + smallBtnText, + largeBtnText, + isSmallBtnDisabled, + isLargeBtnDisabled, + largeBtnType, +}) => { + const theme = useTheme(); + return ( + + {isSmallBtnMissing || ( + + )} + {isLargeBtnMissing || ( + + )} + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsPage.tsx b/src/pages/IntegrationsPage/IntegrationsPage.tsx new file mode 100644 index 00000000..c090b11a --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsPage.tsx @@ -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(false); + const [companyName, setCompanyName] = useState(null); + useEffect(() => { + if (editQuizId === null) navigate("/list"); + }, [navigate, editQuizId]); + const heightBar = heightSidebar + 51 + 88 + 36 + 25; + + if (quiz === undefined) + return ( + + ); + + const handleCloseModal = () => { + setIsModalOpen(false); + // setTimeout(() => { + // setCompanyName(null); + // }, 300); + }; + + return ( + <> + + + Интеграции + + + {/**/} + + + ); +}; diff --git a/src/pages/IntegrationsPage/PartnersBoard/PartnerItem/PartnerItem.tsx b/src/pages/IntegrationsPage/PartnersBoard/PartnerItem/PartnerItem.tsx new file mode 100644 index 00000000..b780ed4e --- /dev/null +++ b/src/pages/IntegrationsPage/PartnersBoard/PartnerItem/PartnerItem.tsx @@ -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 = ({ + partner, + setIsModalOpen, + setCompanyName, +}) => { + const theme = useTheme(); + + const handleClick = () => { + setCompanyName(partner.name); + setIsModalOpen(true); + }; + + return ( + <> + {partner && ( + + {partner.logo ? ( + {partner.name} + ) : ( + {partner.name} + )} + + )} + + ); +}; diff --git a/src/pages/IntegrationsPage/PartnersBoard/PartnersBoard.tsx b/src/pages/IntegrationsPage/PartnersBoard/PartnersBoard.tsx new file mode 100644 index 00000000..adf54a60 --- /dev/null +++ b/src/pages/IntegrationsPage/PartnersBoard/PartnersBoard.tsx @@ -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 = ({ + 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, + ); + + return ( + + {/*{Object.entries(partnersByCategory).map(([category, partners]) => (*/} + {/* */} + {/* */} + {/* {category}*/} + {/* */} + {/* */} + {/* {partners.map((partner) => (*/} + {/* */} + {/* ))}*/} + + {/* */} + {/* */} + {/*))}*/} + + Аналитика + + + + + ); +}; diff --git a/src/pages/IntegrationsPage/mocks/MockData.ts b/src/pages/IntegrationsPage/mocks/MockData.ts new file mode 100644 index 00000000..22094d4f --- /dev/null +++ b/src/pages/IntegrationsPage/mocks/MockData.ts @@ -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", +]; diff --git a/src/pages/IntegrationsPage/mocks/YandexMetric.png b/src/pages/IntegrationsPage/mocks/YandexMetric.png new file mode 100644 index 00000000..96b5ac66 Binary files /dev/null and b/src/pages/IntegrationsPage/mocks/YandexMetric.png differ diff --git a/src/pages/IntegrationsPage/mocks/amoCrmLogo.png b/src/pages/IntegrationsPage/mocks/amoCrmLogo.png new file mode 100644 index 00000000..07d03d78 Binary files /dev/null and b/src/pages/IntegrationsPage/mocks/amoCrmLogo.png differ diff --git a/src/pages/Tariffs/Tariffs.tsx b/src/pages/Tariffs/Tariffs.tsx index d2c6a1a1..650705f5 100644 --- a/src/pages/Tariffs/Tariffs.tsx +++ b/src/pages/Tariffs/Tariffs.tsx @@ -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); } diff --git a/src/pages/ViewPublicationPage.tsx b/src/pages/ViewPublicationPage.tsx index 530cd11f..8dff8673 100644 --- a/src/pages/ViewPublicationPage.tsx +++ b/src/pages/ViewPublicationPage.tsx @@ -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 ( state.currentStep); const { isTestServer } = useDomainDefine(); - useEffect(() => { const getData = async () => { const quizes = await quizApi.getList(); diff --git a/src/ui_kit/CustomTextField.tsx b/src/ui_kit/CustomTextField.tsx index 8b07b20b..5090f98e 100755 --- a/src/ui_kit/CustomTextField.tsx +++ b/src/ui_kit/CustomTextField.tsx @@ -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, diff --git a/src/ui_kit/Sidebar/Sidebar.tsx b/src/ui_kit/Sidebar/Sidebar.tsx index 6f179b66..eee51b68 100755 --- a/src/ui_kit/Sidebar/Sidebar.tsx +++ b/src/ui_kit/Sidebar/Sidebar.tsx @@ -178,6 +178,35 @@ export default function Sidebar({ changePage, disableCollapse }: SidebarProps) { /> } /> + { + navigate("/integrations"); + setCurrentStep(16); + }} + text={"Интеграции"} + isCollapsed={isMenuCollapsed} + isActive={pathname.startsWith("/integrations")} + disabled={ + pathname.startsWith("/integrations") + ? false + : quiz === undefined + ? true + : quiz?.config.type === null + } + icon={ + + } + /> {/* {quizSettingsMenuItems.map((menuItem, index) => { const Icon = menuItem[0]; diff --git a/src/ui_kit/Sidebar/SidebarMobile.tsx b/src/ui_kit/Sidebar/SidebarMobile.tsx index 12645cab..8dee27c4 100644 --- a/src/ui_kit/Sidebar/SidebarMobile.tsx +++ b/src/ui_kit/Sidebar/SidebarMobile.tsx @@ -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 = ({ const [inputOpen, setInputOpen] = useState(false); const quiz = useCurrentQuiz(); const [inputValue, setInputValue] = useState(quiz.name); - const ref = useRef(null); + const ref = useRef(null); const heightSidebar = useRef(null); const navigate = useNavigate(); const { pathname } = useLocation(); @@ -62,15 +62,19 @@ export const SidebarMobile: FC = ({ ); useEffect(() => { - observer.current.observe(heightSidebar.current); + if (heightSidebar.current) { + observer.current.observe(heightSidebar.current); + } }, [heightSidebar, observer]); - const handleClick = (event) => { + const handleClick = (event: ChangeEvent) => { 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 = ({ changePage(index); }; const openPopper = Boolean(anchorEl); - const id = openPopper ? "simple-popper" : undefined; + const id = openPopper ? "simple-popper" : ""; return ( void; changePage: (step: number) => void; - anchorEl: HTMLElement; + anchorEl: HTMLElement | null; id: string; }; export const SidebarModal = ({ diff --git a/src/utils/checkQuestionHint.ts b/src/utils/checkQuestionHint.ts index 9c10a9df..d3460f28 100644 --- a/src/utils/checkQuestionHint.ts +++ b/src/utils/checkQuestionHint.ts @@ -10,7 +10,7 @@ import { import { Quiz } from "@model/quiz/quiz"; export const checkQuestionHint = ( - questions: AnyTypedQuizQuestion, + questions: AnyTypedQuizQuestion[], quiz: Quiz, ): Record => { 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, ); diff --git a/src/utils/deleteFunc.ts b/src/utils/deleteFunc.ts index 580a6ed7..f07db4f1 100644 --- a/src/utils/deleteFunc.ts +++ b/src/utils/deleteFunc.ts @@ -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( + parentResult.content.id, + (item) => { + item.content.usage = true; + }, + ); } else { //почему-то не существует результа у родителя. Создаём. Новосозданные результы видны сразу await createResult(quiz.backendId, parentQuestion.content.id); diff --git a/src/utils/hooks/useYandexMetrica.tsx b/src/utils/hooks/useYandexMetrica.tsx new file mode 100644 index 00000000..a2232993 --- /dev/null +++ b/src/utils/hooks/useYandexMetrica.tsx @@ -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 = `
`; + document.body.appendChild(noscript); + } + }, [yandexMetricNumber]); +};