Merge branch 'add-vk-into-integrations' into 'dev'

Add vk into integrations

See merge request frontend/squiz!294
This commit is contained in:
Nastya 2024-05-01 10:05:18 +00:00
commit 246a090d10
12 changed files with 525 additions and 287 deletions

@ -118,6 +118,11 @@ export interface QuizConfig {
vkMetricNumber: number | undefined;
}
export enum QuizMetricType {
yandex = "yandexMetricNumber",
vk = "vkMetricNumber",
}
export type FormContactFieldName =
| "name"
| "email"

@ -1,41 +0,0 @@
import { Box, useTheme } from "@mui/material";
import { FC } from "react";
import { YandexMetricaLogo } from "../mocks/YandexMetricaLogo";
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)}
>
<YandexMetricaLogo />
</Box>
</>
);
};

@ -1,197 +0,0 @@
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 handleClose = () => {
handleCloseModal();
if (!yandexNumber) {
setIsSave(false);
setCurrentValue("");
return;
}
setIsSave(true);
setCurrentValue(yandexNumber.toString());
};
const handleSave = () => {
handleCloseModal();
updateQuiz(quiz?.id, (quiz) => {
quiz.config.yandexMetricNumber = currentValue
? Number(currentValue)
: undefined;
});
if (!currentValue) {
setIsSave(false);
return;
}
setIsSave(true);
};
const handleEdit = () => {
setIsSave(false);
};
const handleClear = () => {
setCurrentValue("");
setIsSave(false);
};
return (
<Dialog
open={isModalOpen}
onClose={handleClose}
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={handleClose}
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={handleClose}
variant={"outlined"}
>
Отмена
</Button>
<Button
sx={{ width: isMobile ? "100%" : "130px" }}
variant={"contained"}
onClick={handleSave}
>
Сохранить
</Button>
</Box>
)}
</Box>
</Dialog>
);
}

@ -6,6 +6,7 @@ import { useQuizStore } from "@root/quizes/store";
import { useNavigate } from "react-router-dom";
import { PartnersBoard } from "./PartnersBoard/PartnersBoard";
import { partnersMock } from "./mocks/MockData";
import { QuizMetricType } from "@model/quizSettings";
interface IntegrationsPageProps {
heightSidebar: number;
@ -22,7 +23,9 @@ export const IntegrationsPage = ({
const navigate = useNavigate();
const isMobile = useMediaQuery(theme.breakpoints.down(660));
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const [companyName, setCompanyName] = useState<string | null>(null);
const [companyName, setCompanyName] = useState<
keyof typeof QuizMetricType | null
>(null);
useEffect(() => {
if (editQuizId === null) navigate("/list");
}, [navigate, editQuizId]);
@ -62,6 +65,7 @@ export const IntegrationsPage = ({
<PartnersBoard
partners={partnersMock}
setIsModalOpen={setIsModalOpen}
companyName={companyName}
setCompanyName={setCompanyName}
isModalOpen={isModalOpen}
handleCloseModal={handleCloseModal}

@ -0,0 +1,263 @@
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, { useEffect, useMemo, 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";
import { QuizMetricType } from "@model/quizSettings";
import { InstructionsBlock } from "./IntsructionsBlock/InstructionsBlock";
interface Props {
isModalOpen: boolean;
handleCloseModal: () => void;
companyName: keyof typeof QuizMetricType;
}
export default function AnalyticsModal({
isModalOpen,
handleCloseModal,
companyName,
}: Props) {
const theme = useTheme();
const quiz = useCurrentQuiz();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const configName = QuizMetricType[companyName];
const meterNumber = quiz?.config[configName];
const [isSave, setIsSave] = useState<boolean>(!!meterNumber);
const [currentValue, setCurrentValue] = useState<string>(
meterNumber ? meterNumber.toString() : "",
);
const analyticTexts = useMemo(() => {
return {
yandex: {
header: "Яндекс.Метрикой",
counterName: "счетчика",
instructionTitle: "Как установить Яндекс Метрику в квиз?",
instructionSubTitle: "Инструкция по настройке Яндекс Метрики",
instructionHeader: "Настройка счётчика и интеграции",
instructionText:
"Повседневная практика показывает, что дальнейшее развитие различных форм деятельности требуют определения и уточнения соответствующий условий активизации.Повседневная практика показывает, что дальнейшее развитие различных форм деятельности требуют определения и уточнения соответствующий условий активизации.Повседневная практика показывает, что дальнейшее развитие различных форм деятельности требуют определения и уточнения соответствующий условий активизации.",
},
vk: {
header: "VK Пиксель",
counterName: "пикселя",
instructionTitle: "Как установить VK Пиксель в квиз?",
instructionSubTitle: "Инструкция по настройке VK Пиксель",
instructionHeader: "Настройка счётчика и интеграции",
instructionText:
"Повседневная практика показывает, что дальнейшее развитие различных форм деятельности требуют определения и уточнения соответствующий условий активизации.",
},
};
}, []);
const handleClose = () => {
handleCloseModal();
if (!meterNumber) {
setIsSave(false);
setCurrentValue("");
return;
}
setIsSave(true);
setCurrentValue(meterNumber.toString());
};
const handleSave = () => {
handleCloseModal();
updateQuiz(quiz?.id, (quiz) => {
quiz.config[configName] = currentValue ? Number(currentValue) : undefined;
});
if (!currentValue) {
setIsSave(false);
return;
}
setIsSave(true);
};
const handleEdit = () => {
setIsSave(false);
};
const handleClear = () => {
setCurrentValue("");
setIsSave(false);
};
useEffect(() => {
const configName =
QuizMetricType[companyName as keyof typeof QuizMetricType];
const meterNumber = quiz?.config[configName];
setCurrentValue(meterNumber ? meterNumber.toString() : "");
setIsSave(!!meterNumber);
}, [companyName]);
return (
<Dialog
open={isModalOpen}
onClose={handleClose}
fullWidth
PaperProps={{
sx: {
maxWidth: isTablet ? "100%" : "920px",
borderRadius: "12px",
},
}}
>
<Box
sx={{
width: "100%",
height: "68px",
backgroundColor: theme.palette.background.default,
}}
>
<Typography
sx={{
fontSize: isMobile ? "20px" : "24px",
fontWeight: "500",
padding: "20px",
color: theme.palette.grey2.main,
}}
>
Аналитика с {analyticTexts[companyName]?.header}
</Typography>
</Box>
<IconButton
onClick={handleClose}
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: "20px 20px",
flexGrow: 1,
gap: "20px",
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "10px",
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
maxWidth: isMobile ? "100%" : "590px",
position: "relative",
}}
>
<Typography fontWeight={500}>
{isSave
? `Ваш номер ${analyticTexts[companyName]?.counterName}`
: `Введите номер ${analyticTexts[companyName]?.counterName}`}
</Typography>
{isSave && (
<Box
sx={{
display: "flex",
gap: "10px",
position: "absolute",
right: "0",
top: "0",
}}
>
<IconButton onClick={handleEdit} sx={{ padding: "0" }}>
<EditPencil
color={theme.palette.brightPurple.main}
width={"24px"}
height={"24px"}
/>
</IconButton>
<IconButton onClick={handleClear} sx={{ padding: "0" }}>
<Trash
sx={{
width: "24px",
"& path": {
stroke: theme.palette.brightPurple.main,
},
}}
/>
</IconButton>
</Box>
)}
</Box>
<Box
sx={{
display: "flex",
flexDirection: isMobile ? "column" : "row",
gap: "20px",
alignItems: "center",
}}
>
<CustomTextField
sxForm={{ maxWidth: isMobile ? "100%" : "590px" }}
placeholder={isSave ? currentValue : "в формате ХХХХХХХХ"}
type={"number"}
value={currentValue}
disabled={isSave}
onChange={(e) => {
const onlyNums = e.target.value.replace(/[^0-9]/g, "");
setCurrentValue(onlyNums);
}}
/>
{!isSave && (
<Box
sx={{
width: isMobile ? "100%" : "auto",
display: "flex",
justifyContent: isMobile ? "space-between" : "end",
gap: "10px",
}}
>
{meterNumber && !isSave && (
<Button
sx={{ width: isMobile ? "100%" : "130px", height: "44px" }}
onClick={handleClose}
variant={"outlined"}
>
Отмена
</Button>
)}
<Button
sx={{ width: isMobile ? "100%" : "130px", height: "44px" }}
variant={"contained"}
onClick={handleSave}
>
Сохранить
</Button>
</Box>
)}
</Box>
</Box>
<InstructionsBlock
headerText={analyticTexts[companyName]?.instructionTitle}
subHeaderText={analyticTexts[companyName]?.instructionSubTitle}
instructionTitle={analyticTexts[companyName]?.instructionHeader}
instructionsText={analyticTexts[companyName]?.instructionText}
/>
</Box>
</Dialog>
);
}

@ -0,0 +1,88 @@
import Box from "@mui/material/Box";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import { Typography, useMediaQuery, useTheme } from "@mui/material";
import React, { FC, useState } from "react";
type InstructionsBlockProps = {
headerText: string;
subHeaderText: string;
instructionTitle: string;
instructionsText: string;
};
export const InstructionsBlock: FC<InstructionsBlockProps> = ({
headerText,
instructionsText,
subHeaderText,
instructionTitle,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const [isInstructionOpen, setIsInstructionOpen] = useState(false);
return (
<Box>
<Box
onClick={() => setIsInstructionOpen(!isInstructionOpen)}
sx={{
cursor: "pointer",
backgroundColor: theme.palette.background.default,
borderRadius: isInstructionOpen ? "12px 12px 0 0" : "12px",
padding: "20px",
border: "1px solid #E5E5E5",
borderBottom: isInstructionOpen ? "none" : "1px solid #E5E5E5",
position: "relative",
}}
>
{isMobile && (
<KeyboardArrowUpIcon
fontSize="medium"
sx={{
position: "absolute",
top: "20px",
right: "20px",
transition: "transform 0.3s",
transform: isInstructionOpen ? "rotate(0deg" : "rotate(180deg)",
}}
/>
)}
<Typography
sx={{
marginBottom: "4px",
fontWeight: "500",
width: isMobile ? "90%" : "100%",
}}
>
{headerText}
</Typography>
<Typography sx={{ fontSize: "16px", color: theme.palette.grey2.main }}>
{subHeaderText}
</Typography>
</Box>
{isInstructionOpen && (
<Box
sx={{
borderRadius: " 0 0 12px 12px",
padding: "20px",
border: "1px solid #E5E5E5",
borderTop: isInstructionOpen ? "none" : "1px solid #E5E5E5",
maxHeight: isMobile ? "240px" : "300px",
overflowY: "auto",
}}
>
<Typography
sx={{
fontWeight: "500",
color: theme.palette.grey3.main,
marginBottom: "15px",
}}
>
{instructionTitle}
</Typography>
<Typography sx={{ color: theme.palette.grey3.main }}>
{instructionsText}
</Typography>
</Box>
)}
</Box>
);
};

@ -1,7 +1,10 @@
import { Box, Typography, useTheme } from "@mui/material";
import { FC } from "react";
import { YandexButton } from "../IntegrationYandex/YandexButton";
import YandexModal from "../IntegrationYandex/YandexModal";
import { ServiceButton } from "./ServiceButton/ServiceButton";
import { YandexMetricaLogo } from "../mocks/YandexMetricaLogo";
import AnalyticsModal from "./AnalyticsModal/AnalyticsModal";
import { VKPixelLogo } from "../mocks/VKPixelLogo";
import { QuizMetricType } from "@model/quizSettings";
export type Partner = {
name: string;
@ -12,7 +15,8 @@ export type Partner = {
type PartnersBoardProps = {
partners: Partner[];
setIsModalOpen: (value: boolean) => void;
setCompanyName: (value: string) => void;
companyName: keyof typeof QuizMetricType | null;
setCompanyName: (value: keyof typeof QuizMetricType) => void;
isModalOpen: boolean;
handleCloseModal: () => void;
};
@ -22,17 +26,18 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
setIsModalOpen,
isModalOpen,
handleCloseModal,
companyName,
setCompanyName,
}) => {
const theme = useTheme();
const partnersByCategory = partners.reduce(
(acc, partner) => {
(acc[partner.category] = acc[partner.category] || []).push(partner);
return acc;
},
{} as Record<string, Partner[]>,
);
// const partnersByCategory = partners.reduce(
// (acc, partner) => {
// (acc[partner.category] = acc[partner.category] || []).push(partner);
// return acc;
// },
// {} as Record<string, Partner[]>,
// );
return (
<Box
@ -74,21 +79,47 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
{/* </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>
<Typography
variant="h6"
sx={{
textAlign: { xs: "start", sm: "start", md: "start" },
lineHeight: "1",
marginBottom: "12px",
}}
>
Аналитика
</Typography>
<Box
sx={{
display: "flex",
flexWrap: "wrap",
justifyContent: { xs: "start", sm: "start", md: "start" },
}}
>
<ServiceButton
logo={<YandexMetricaLogo />}
setIsModalOpen={setIsModalOpen}
name={"yandex"}
setCompanyName={setCompanyName}
/>
<ServiceButton
logo={<VKPixelLogo />}
title={"VK Пиксель"}
name={"vk"}
setIsModalOpen={setIsModalOpen}
setCompanyName={setCompanyName}
></ServiceButton>
</Box>
</Box>
{companyName && (
<AnalyticsModal
isModalOpen={isModalOpen}
handleCloseModal={handleCloseModal}
companyName={companyName}
/>
)}
</Box>
);
};

@ -0,0 +1,57 @@
import { Box, Typography, useTheme } from "@mui/material";
import { FC } from "react";
import { QuizMetricType } from "@model/quizSettings";
type PartnerItemProps = {
setIsModalOpen: (value: boolean) => void;
setCompanyName: (value: keyof typeof QuizMetricType) => void;
logo?: JSX.Element;
title?: string;
name: string;
};
export const ServiceButton: FC<PartnerItemProps> = ({
setIsModalOpen,
logo,
title,
name,
setCompanyName,
}) => {
const theme = useTheme();
const handleClick = () => {
setCompanyName(name as keyof typeof QuizMetricType);
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={handleClick}
>
{logo && logo}
<Typography
sx={{
fontSize: "18px",
fontWeight: "400",
marginLeft: "15px",
}}
>
{title && title}
</Typography>
</Box>
</>
);
};

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 65 KiB

@ -0,0 +1,6 @@
import React from "react";
import { ReactComponent as VKLogo } from "./VKLogo.svg";
export const VKPixelLogo = () => {
return <VKLogo />;
};

@ -50,19 +50,38 @@ export default function AvailablePrivilege() {
const squizHideBadge = userPrivileges?.squizHideBadge?.amount || 0;
//Где дни - amount - это на сколько дней выдан безлимит. т.е. не сколько осталось, а на сколько дней выдано
function getCramps (amount: number, created_at: string) {
if (created_at.length === 0) return 0
const currentDate = moment()
function getCramps(amount: number, created_at: string) {
if (created_at.length === 0) return 0;
const currentDate = moment();
return Number((moment(moment(created_at).add(amount, "days").diff(currentDate)).unix() / 86400).toFixed(1))
return Number(
(
moment(
moment(created_at).add(amount, "days").diff(currentDate),
).unix() / 86400
).toFixed(1),
);
}
const quizUnlimDays = getCramps(quizUnlimTime, userPrivileges?.quizUnlimTime?.created_at || "")
const squizBadgeDays = getCramps(squizHideBadge, userPrivileges?.squizHideBadge?.created_at || "")
const quizUnlimDays = getCramps(
quizUnlimTime,
userPrivileges?.quizUnlimTime?.created_at || "",
);
const squizBadgeDays = getCramps(
squizHideBadge,
userPrivileges?.squizHideBadge?.created_at || "",
);
const currentDate = moment()
console.log(quizUnlimDays)
console.log(moment())
console.log(moment(moment(userPrivileges?.quizUnlimTime?.created_at).add(quizUnlimTime, "days")))
const currentDate = moment();
console.log(quizUnlimDays);
console.log(moment());
console.log(
moment(
moment(userPrivileges?.quizUnlimTime?.created_at).add(
quizUnlimTime,
"days",
),
),
);
return (
<Box
@ -82,12 +101,9 @@ export default function AvailablePrivilege() {
<Typography variant={"body1"} sx={{ color: "#4D4D4D" }}>
Безлимитные заявки:{" "}
<strong>
{
quizUnlimDays > 0 && quizUnlimDays < 1 ?
"последний день"
:
`${Math.trunc(quizUnlimDays)} ${declOfNum(Math.trunc(quizUnlimDays), DayForm)}`
}
{quizUnlimDays > 0 && quizUnlimDays < 1
? "последний день"
: `${Math.trunc(quizUnlimDays)} ${declOfNum(Math.trunc(quizUnlimDays), DayForm)}`}
</strong>
</Typography>
{quizCnt !== 0 && (
@ -99,12 +115,9 @@ export default function AvailablePrivilege() {
<Typography variant={"body1"} sx={{ color: "#4D4D4D" }}>
Скрытие логотипа PenaQuiz:{" "}
<strong>
{
squizBadgeDays > 0 && squizBadgeDays < 1 ?
"последний день"
:
`${Math.trunc(squizBadgeDays)} ${declOfNum(Math.trunc(squizBadgeDays), DayForm)}`
}
{squizBadgeDays > 0 && squizBadgeDays < 1
? "последний день"
: `${Math.trunc(squizBadgeDays)} ${declOfNum(Math.trunc(squizBadgeDays), DayForm)}`}
</strong>
</Typography>
)}

@ -72,7 +72,6 @@ export const SidebarMobile: FC<Iprops> = ({
};
const clickInput = (event: MouseEvent) => {
debugger;
if (ref.current && !ref.current?.contains(event.target as Node))
setInputOpen(false);
};