рефакторинг тарифов
This commit is contained in:
parent
572dfae016
commit
66456bac31
@ -11,8 +11,8 @@ import { useSnackbar } from "notistack";
|
||||
import { PayModal } from "./PayModal";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { cartApi } from "@/api/cart";
|
||||
import { outCart } from "../Tariffs/Tariffs";
|
||||
import { inCart } from "../Tariffs/Tariffs";
|
||||
import { outCart } from "../Tariffs/utils";
|
||||
import { inCart } from "../Tariffs/utils";
|
||||
import { isTestServer } from "@/utils/hooks/useDomainDefine";
|
||||
import { useToken } from "@frontend/kitui";
|
||||
import { useSWRConfig } from "swr";
|
||||
|
147
src/pages/Tariffs/TariffCardDisplaySelector.tsx
Normal file
147
src/pages/Tariffs/TariffCardDisplaySelector.tsx
Normal file
@ -0,0 +1,147 @@
|
||||
import { Box, useMediaQuery, useTheme } from "@mui/material"
|
||||
import { NavCard } from "./components/NavCard"
|
||||
import { createTariffElements } from "./tariffsUtils/createTariffElements"
|
||||
import SmallIconPena from "@/assets/icons/SmallIconPena"
|
||||
|
||||
interface TariffCardDisplaySelectorProps {
|
||||
content: {
|
||||
title: string,
|
||||
onClick: () => void
|
||||
}[]
|
||||
selectedItem: TypePages
|
||||
tariffs: any[]
|
||||
user: any
|
||||
discounts: any[]
|
||||
openModalHC: (tariffInfo: any) => void
|
||||
userPrivilegies: any
|
||||
startRequestCreate: () => void
|
||||
}
|
||||
|
||||
export const TariffCardDisplaySelector = ({
|
||||
content,
|
||||
selectedItem,
|
||||
tariffs,
|
||||
user,
|
||||
discounts,
|
||||
openModalHC,
|
||||
userPrivilegies,
|
||||
startRequestCreate
|
||||
}: TariffCardDisplaySelectorProps) => {
|
||||
const theme = useTheme()
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
const sendRequest = userPrivilegies?.quizManual?.amount > 0 ? startRequestCreate : undefined
|
||||
|
||||
switch (selectedItem) {
|
||||
case "dop":
|
||||
return <Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
width: "100%"
|
||||
}}>
|
||||
{content.map(data => <NavCard {...data} key={data.title} />)}
|
||||
</Box>
|
||||
|
||||
case "hide":
|
||||
const filteredBadgeTariffs = tariffs.filter((tariff) => {
|
||||
return (
|
||||
tariff.privileges[0].serviceKey === "squiz" &&
|
||||
!tariff.isDeleted &&
|
||||
!tariff.isCustom &&
|
||||
tariff.privileges[0].privilegeId === "squizHideBadge" &&
|
||||
tariff.privileges[0]?.type === "day"
|
||||
);
|
||||
});
|
||||
return createTariffElements(
|
||||
filteredBadgeTariffs,
|
||||
false,
|
||||
user,
|
||||
discounts,
|
||||
openModalHC,
|
||||
)
|
||||
|
||||
case "create":
|
||||
const filteredCreateTariffs = tariffs.filter((tariff) => {
|
||||
return (
|
||||
tariff.privileges[0].serviceKey === "squiz" &&
|
||||
!tariff.isDeleted &&
|
||||
!tariff.isCustom &&
|
||||
tariff.privileges[0].privilegeId === "quizManual" &&
|
||||
tariff.privileges[0]?.type === "count"
|
||||
);
|
||||
});
|
||||
return createTariffElements(
|
||||
filteredCreateTariffs,
|
||||
false,
|
||||
user,
|
||||
discounts,
|
||||
openModalHC,
|
||||
sendRequest,
|
||||
true,
|
||||
<SmallIconPena />
|
||||
)
|
||||
|
||||
case "premium":
|
||||
const filteredPremiumTariffs = tariffs.filter((tariff) => {
|
||||
return (
|
||||
tariff.privileges[0].serviceKey === "squiz" &&
|
||||
!tariff.isDeleted &&
|
||||
!tariff.isCustom &&
|
||||
tariff.privileges[0].privilegeId === "squizPremium" &&
|
||||
tariff.privileges[0]?.type === "day"
|
||||
);
|
||||
});
|
||||
return createTariffElements(
|
||||
filteredPremiumTariffs,
|
||||
false,
|
||||
user,
|
||||
discounts,
|
||||
openModalHC,
|
||||
)
|
||||
|
||||
case "analytics":
|
||||
const filteredAnalyticsTariffs = tariffs.filter((tariff) => {
|
||||
return (
|
||||
tariff.privileges[0].serviceKey === "squiz" &&
|
||||
!tariff.isDeleted &&
|
||||
!tariff.isCustom &&
|
||||
tariff.privileges[0].privilegeId === "squizAnalytics" &&
|
||||
tariff.privileges[0]?.type === "count"
|
||||
);
|
||||
});
|
||||
return createTariffElements(
|
||||
filteredAnalyticsTariffs,
|
||||
false,
|
||||
user,
|
||||
discounts,
|
||||
openModalHC,
|
||||
)
|
||||
|
||||
case "custom":
|
||||
const filteredCustomTariffs = tariffs.filter((tariff) => {
|
||||
return (
|
||||
tariff.privileges[0].serviceKey === "squiz" &&
|
||||
!tariff.isDeleted &&
|
||||
tariff.isCustom &&
|
||||
tariff.privileges[0]?.type === "day"
|
||||
);
|
||||
});
|
||||
return createTariffElements(
|
||||
filteredCustomTariffs,
|
||||
false,
|
||||
user,
|
||||
discounts,
|
||||
openModalHC,
|
||||
)
|
||||
|
||||
default:
|
||||
return <Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
width: "100%"
|
||||
}}>
|
||||
{content.map(data => <NavCard {...data} key={data.title} />)}
|
||||
</Box>
|
||||
}
|
||||
}
|
@ -1,39 +1,33 @@
|
||||
import { activatePromocode } from "@api/promocode";
|
||||
import { useToken } from "@frontend/kitui";
|
||||
import ArrowLeft from "@icons/questionsPage/arrowLeft";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
IconButton,
|
||||
Modal,
|
||||
Paper,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useUserStore } from "@root/user";
|
||||
import { LogoutButton } from "@ui_kit/LogoutButton";
|
||||
import { useDomainDefine } from "@utils/hooks/useDomainDefine";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useEffect, useState } from "react";
|
||||
import { withErrorBoundary } from "react-error-boundary";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import Logotip from "../../pages/Landing/images/icons/QuizLogo";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import CollapsiblePromocodeField from "./CollapsiblePromocodeField";
|
||||
import { Tabs } from "./Tabs";
|
||||
import { createTariffElements } from "./tariffsUtils/createTariffElements";
|
||||
import { currencyFormatter } from "./tariffsUtils/currencyFormatter";
|
||||
import { useWallet, setCash } from "@root/cash";
|
||||
import { handleLogoutClick } from "@utils/HandleLogoutClick";
|
||||
import { cartApi } from "@api/cart";
|
||||
|
||||
import { Other } from "./pages/Other";
|
||||
import { TariffCardDisplaySelector } from "./TariffCardDisplaySelector";
|
||||
import { ModalRequestCreate } from "./ModalRequestCreate";
|
||||
import { cancelCC, useCC } from "@/stores/cc";
|
||||
import { NavSelect } from "./NavSelect";
|
||||
import { useTariffs } from '@utils/hooks/useTariffs';
|
||||
import { useDiscounts } from '@utils/hooks/useDiscounts';
|
||||
import { PaymentConfirmationModal } from "./components/PaymentConfirmationModal";
|
||||
import { TariffsHeader } from "./components/TariffsHeader";
|
||||
import { inCart, outCart } from "./utils";
|
||||
|
||||
const StepperText: Record<string, string> = {
|
||||
day: "Тарифы на время",
|
||||
@ -50,9 +44,9 @@ function TariffPage() {
|
||||
const userId = useUserStore((state) => state.userId);
|
||||
const navigate = useNavigate();
|
||||
const user = useUserStore((state) => state.customerAccount);
|
||||
const a = useUserStore((state) => state.customerAccount); //c wallet
|
||||
const userWithWallet = useUserStore((state) => state.customerAccount); //c wallet
|
||||
console.log("________________34563875693785692576_____________USERRRRRRR")
|
||||
console.log(a)
|
||||
// console.log(userWithWallet)
|
||||
const { data: discounts } = useDiscounts(userId);
|
||||
const [isRequestCreate, setIsRequestCreate] = useState(false);
|
||||
const [openModal, setOpenModal] = useState({});
|
||||
@ -69,13 +63,13 @@ console.log("________34563875693785692576_____ TARIFFS")
|
||||
console.log(tariffs)
|
||||
|
||||
useEffect(() => {
|
||||
if (a) {
|
||||
if (userWithWallet) {
|
||||
let cs = currencyFormatter.format(Number(user.wallet.cash) / 100);
|
||||
let cc = Number(user.wallet.cash);
|
||||
let cr = Number(user.wallet.cash) / 100;
|
||||
setCash(cs, cc, cr);
|
||||
}
|
||||
}, [a]);
|
||||
}, [userWithWallet]);
|
||||
|
||||
useEffect(() => {
|
||||
if (cc) {
|
||||
@ -169,63 +163,10 @@ console.log(tariffs)
|
||||
setIsRequestCreate(true)
|
||||
}
|
||||
|
||||
if (!a) return null;
|
||||
if (!userWithWallet) return null;
|
||||
return (
|
||||
<>
|
||||
<Container
|
||||
component="nav"
|
||||
disableGutters
|
||||
maxWidth={false}
|
||||
sx={{
|
||||
px: "16px",
|
||||
display: "flex",
|
||||
height: "80px",
|
||||
alignItems: "center",
|
||||
gap: isMobile ? "7px" : isTablet ? "20px" : "60px",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
bgcolor: "white",
|
||||
borderBottom: "1px solid #E3E3E3",
|
||||
}}
|
||||
>
|
||||
<Link to="/">
|
||||
<Logotip width={124} />
|
||||
</Link>
|
||||
<IconButton onClick={() => navigate("/list")}>
|
||||
<ArrowLeft color="black" />
|
||||
</IconButton>
|
||||
<Box sx={{ display: "flex", ml: "auto" }}>
|
||||
<Box sx={{ whiteSpace: "nowrap" }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
lineHeight: "14px",
|
||||
color: "gray",
|
||||
}}
|
||||
>
|
||||
Мой баланс
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={"#7e2aea"}
|
||||
fontSize={
|
||||
isMobile ? (cashString.length > 9 ? "13px" : "16px") : "16px"
|
||||
}
|
||||
>
|
||||
{cashString}
|
||||
</Typography>
|
||||
</Box>
|
||||
<LogoutButton
|
||||
onClick={() => {
|
||||
navigate("/");
|
||||
handleLogoutClick();
|
||||
}}
|
||||
sx={{
|
||||
ml: "20px",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Container>
|
||||
<TariffsHeader cashString={cashString} />
|
||||
<Box
|
||||
sx={{
|
||||
p: "25px",
|
||||
@ -281,9 +222,9 @@ console.log(tariffs)
|
||||
discounts,
|
||||
openModalHC,
|
||||
)}
|
||||
{(selectedItem === "dop" || selectedItem === "hide" || selectedItem === "create")
|
||||
{(selectedItem === "hide" || selectedItem === "create" || selectedItem === "premium" || selectedItem === "analytics" || selectedItem === "custom")
|
||||
&& (
|
||||
<Other
|
||||
<TariffCardDisplaySelector
|
||||
selectedItem={selectedItem}
|
||||
content={[
|
||||
{
|
||||
@ -294,8 +235,67 @@ console.log(tariffs)
|
||||
title: "Создать квиз на заказ",
|
||||
onClick: () => setSelectedItem("create")
|
||||
},
|
||||
{
|
||||
title: "Премиум функции",
|
||||
onClick: () => setSelectedItem("premium")
|
||||
},
|
||||
{
|
||||
title: "Расширенная аналитика",
|
||||
onClick: () => setSelectedItem("analytics")
|
||||
},
|
||||
{
|
||||
title: "Кастомные тарифы",
|
||||
onClick: () => setSelectedItem("custom")
|
||||
},
|
||||
]}
|
||||
|
||||
tariffs={tariffs}
|
||||
user={user}
|
||||
discounts={discounts}
|
||||
openModalHC={openModalHC}
|
||||
userPrivilegies={userPrivilegies}
|
||||
startRequestCreate={startRequestCreate}
|
||||
/>
|
||||
)}
|
||||
{selectedItem === "dop" && (
|
||||
<TariffCardDisplaySelector
|
||||
selectedItem={selectedItem}
|
||||
content={
|
||||
selectedItem === "dop"
|
||||
? [
|
||||
{
|
||||
title: `Убрать логотип "PenaQuiz"`,
|
||||
onClick: () => setSelectedItem("hide")
|
||||
},
|
||||
{
|
||||
title: "Создать квиз на заказ",
|
||||
onClick: () => setSelectedItem("create")
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: `Убрать логотип "PenaQuiz"`,
|
||||
onClick: () => setSelectedItem("hide")
|
||||
},
|
||||
{
|
||||
title: "Создать квиз на заказ",
|
||||
onClick: () => setSelectedItem("create")
|
||||
},
|
||||
{
|
||||
title: "Премиум функции",
|
||||
onClick: () => setSelectedItem("premium")
|
||||
},
|
||||
{
|
||||
title: "Расширенная аналитика",
|
||||
onClick: () => setSelectedItem("analytics")
|
||||
},
|
||||
{
|
||||
title: "Кастомные тарифы",
|
||||
onClick: () => setSelectedItem("custom")
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
tariffs={tariffs}
|
||||
user={user}
|
||||
discounts={discounts}
|
||||
@ -305,37 +305,12 @@ console.log(tariffs)
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Modal
|
||||
<PaymentConfirmationModal
|
||||
open={Object.values(openModal).length > 0}
|
||||
onClose={() => setOpenModal({})}
|
||||
>
|
||||
<Paper
|
||||
sx={{
|
||||
position: "absolute" as "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
id="modal-modal-title"
|
||||
variant="h6"
|
||||
component="h2"
|
||||
mb="20px"
|
||||
>
|
||||
Вы подтверждаете платёж в сумму{" "}
|
||||
{openModal.price ? openModal.price.toFixed(2) : 0} ₽
|
||||
</Typography>
|
||||
<Button variant="contained" onClick={() => tryBuy(openModal)}>
|
||||
купить
|
||||
</Button>
|
||||
</Paper>
|
||||
</Modal>
|
||||
onConfirm={() => tryBuy(openModal)}
|
||||
price={openModal.price}
|
||||
/>
|
||||
<ModalRequestCreate open={isRequestCreate} onClose={() => setIsRequestCreate(false)} />
|
||||
</>
|
||||
);
|
||||
@ -364,47 +339,3 @@ const LoadingPage = () => (
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const inCart = () => {
|
||||
let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]");
|
||||
if (Array.isArray(saveCart)) {
|
||||
saveCart.forEach(async (id: string) => {
|
||||
const [_, addError] = await cartApi.add(id);
|
||||
|
||||
if (addError) {
|
||||
console.error(addError);
|
||||
} else {
|
||||
let index = saveCart.indexOf("green");
|
||||
|
||||
if (index !== -1) {
|
||||
saveCart.splice(index, 1);
|
||||
}
|
||||
|
||||
localStorage.setItem("saveCart", JSON.stringify(saveCart));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
localStorage.setItem("saveCart", "[]");
|
||||
}
|
||||
};
|
||||
export const outCart = (cart: string[]) => {
|
||||
//Сделаем муторно и подольше, зато при прерывании сессии данные потеряются минимально
|
||||
if (cart.length > 0) {
|
||||
cart.forEach(async (id: string) => {
|
||||
const [_, deleteError] = await cartApi.delete(id);
|
||||
|
||||
if (deleteError) {
|
||||
console.error(deleteError);
|
||||
cancelCC()//мы хотели открыть модалку после покупки тарифа на создание квиза, но не вышло и модалку не откроем
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]") || [];
|
||||
if (!Array.isArray(saveCart)) saveCart = []
|
||||
saveCart = saveCart.push(id);
|
||||
localStorage.setItem("saveCart", JSON.stringify(saveCart));
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
51
src/pages/Tariffs/components/PaymentConfirmationModal.tsx
Normal file
51
src/pages/Tariffs/components/PaymentConfirmationModal.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
Paper,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
|
||||
interface PaymentConfirmationModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
price: number;
|
||||
}
|
||||
|
||||
export const PaymentConfirmationModal = ({
|
||||
open,
|
||||
onClose,
|
||||
onConfirm,
|
||||
price,
|
||||
}: PaymentConfirmationModalProps) => {
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Paper
|
||||
sx={{
|
||||
position: "absolute" as "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
id="modal-modal-title"
|
||||
variant="h6"
|
||||
component="h2"
|
||||
mb="20px"
|
||||
>
|
||||
Вы подтверждаете платёж в сумму{" "}
|
||||
{price ? price.toFixed(2) : 0} ₽
|
||||
</Typography>
|
||||
<Button variant="contained" onClick={onConfirm}>
|
||||
купить
|
||||
</Button>
|
||||
</Paper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
83
src/pages/Tariffs/components/TariffsHeader.tsx
Normal file
83
src/pages/Tariffs/components/TariffsHeader.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import { useToken } from "@frontend/kitui";
|
||||
import ArrowLeft from "@icons/questionsPage/arrowLeft";
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
IconButton,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useUserStore } from "@root/user";
|
||||
import { LogoutButton } from "@ui_kit/LogoutButton";
|
||||
import { handleLogoutClick } from "@utils/HandleLogoutClick";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import Logotip from "../../../pages/Landing/images/icons/QuizLogo";
|
||||
|
||||
interface TariffsHeaderProps {
|
||||
cashString: string;
|
||||
}
|
||||
|
||||
export const TariffsHeader = ({ cashString }: TariffsHeaderProps) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
|
||||
return (
|
||||
<Container
|
||||
component="nav"
|
||||
disableGutters
|
||||
maxWidth={false}
|
||||
sx={{
|
||||
px: "16px",
|
||||
display: "flex",
|
||||
height: "80px",
|
||||
alignItems: "center",
|
||||
gap: isMobile ? "7px" : isTablet ? "20px" : "60px",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
bgcolor: "white",
|
||||
borderBottom: "1px solid #E3E3E3",
|
||||
}}
|
||||
>
|
||||
<Link to="/">
|
||||
<Logotip width={124} />
|
||||
</Link>
|
||||
<IconButton onClick={() => navigate("/list")}>
|
||||
<ArrowLeft color="black" />
|
||||
</IconButton>
|
||||
<Box sx={{ display: "flex", ml: "auto" }}>
|
||||
<Box sx={{ whiteSpace: "nowrap" }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
lineHeight: "14px",
|
||||
color: "gray",
|
||||
}}
|
||||
>
|
||||
Мой баланс
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={"#7e2aea"}
|
||||
fontSize={
|
||||
isMobile ? (cashString.length > 9 ? "13px" : "16px") : "16px"
|
||||
}
|
||||
>
|
||||
{cashString}
|
||||
</Typography>
|
||||
</Box>
|
||||
<LogoutButton
|
||||
onClick={() => {
|
||||
navigate("/");
|
||||
handleLogoutClick();
|
||||
}}
|
||||
sx={{
|
||||
ml: "20px",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
@ -1,97 +0,0 @@
|
||||
import { Box, useMediaQuery, useTheme } from "@mui/material"
|
||||
import { NavCard } from "../components/NavCard"
|
||||
import { createTariffElements } from "../tariffsUtils/createTariffElements"
|
||||
import SmallIconPena from "@/assets/icons/SmallIconPena"
|
||||
|
||||
interface Props {
|
||||
content: {
|
||||
title: string,
|
||||
onClick: () => void
|
||||
}[]
|
||||
selectedItem: TypePages
|
||||
}
|
||||
|
||||
export const Other = ({
|
||||
content,
|
||||
selectedItem,
|
||||
|
||||
tariffs,
|
||||
user,
|
||||
discounts,
|
||||
openModalHC,
|
||||
userPrivilegies,
|
||||
startRequestCreate
|
||||
}: any) => {
|
||||
const theme = useTheme()
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
const sendRequest = userPrivilegies?.quizManual?.amount > 0 ? startRequestCreate : undefined
|
||||
|
||||
switch (selectedItem) {
|
||||
case "hide":
|
||||
const filteredBadgeTariffs = tariffs.filter((tariff) => {
|
||||
return (
|
||||
tariff.privileges[0].serviceKey === "squiz" &&
|
||||
!tariff.isDeleted &&
|
||||
!tariff.isCustom &&
|
||||
tariff.privileges[0].privilegeId === "squizHideBadge" &&
|
||||
tariff.privileges[0]?.type === "day"
|
||||
);
|
||||
});
|
||||
return <Box
|
||||
sx={{
|
||||
justifyContent: "left",
|
||||
display: "grid",
|
||||
gap: "40px",
|
||||
gridTemplateColumns: `repeat(auto-fit, minmax(300px, ${isTablet ? "436px" : "360px"
|
||||
}))`,
|
||||
}}
|
||||
>
|
||||
{createTariffElements(
|
||||
filteredBadgeTariffs,
|
||||
false,
|
||||
user,
|
||||
discounts,
|
||||
openModalHC,
|
||||
)}
|
||||
</Box>
|
||||
case "create":
|
||||
const filteredCreateTariffs = tariffs.filter((tariff) => {
|
||||
return (
|
||||
tariff.privileges[0].serviceKey === "squiz" &&
|
||||
!tariff.isDeleted &&
|
||||
!tariff.isCustom &&
|
||||
tariff.privileges[0].privilegeId === "quizManual" &&
|
||||
tariff.privileges[0]?.type === "count"
|
||||
);
|
||||
});
|
||||
return <Box
|
||||
sx={{
|
||||
justifyContent: "left",
|
||||
display: "grid",
|
||||
gap: "40px",
|
||||
gridTemplateColumns: `repeat(auto-fit, minmax(300px, ${isTablet ? "436px" : "360px"
|
||||
}))`,
|
||||
}}
|
||||
>
|
||||
{createTariffElements(
|
||||
filteredCreateTariffs,
|
||||
false,
|
||||
user,
|
||||
discounts,
|
||||
openModalHC,
|
||||
sendRequest,
|
||||
true,
|
||||
<SmallIconPena/>
|
||||
)}
|
||||
</Box>
|
||||
default:
|
||||
return <Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
width: "100%"
|
||||
}}>
|
||||
{content.map(data => <NavCard {...data} key={data.title} />)}
|
||||
</Box>
|
||||
}
|
||||
}
|
@ -1 +1 @@
|
||||
type TypePages = "count" | "day" | "dop" | "hide" | "create"
|
||||
type TypePages = "count" | "day" | "dop" | "hide" | "create" | "premium" | "analytics" | "custom"
|
46
src/pages/Tariffs/utils.ts
Normal file
46
src/pages/Tariffs/utils.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { cartApi } from "@api/cart";
|
||||
import { cancelCC } from "@/stores/cc";
|
||||
|
||||
export const inCart = () => {
|
||||
let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]");
|
||||
if (Array.isArray(saveCart)) {
|
||||
saveCart.forEach(async (id: string) => {
|
||||
const [_, addError] = await cartApi.add(id);
|
||||
|
||||
if (addError) {
|
||||
console.error(addError);
|
||||
} else {
|
||||
let index = saveCart.indexOf("green");
|
||||
|
||||
if (index !== -1) {
|
||||
saveCart.splice(index, 1);
|
||||
}
|
||||
|
||||
localStorage.setItem("saveCart", JSON.stringify(saveCart));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
localStorage.setItem("saveCart", "[]");
|
||||
}
|
||||
};
|
||||
|
||||
export const outCart = (cart: string[]) => {
|
||||
//Сделаем муторно и подольше, зато при прерывании сессии данные потеряются минимально
|
||||
if (cart.length > 0) {
|
||||
cart.forEach(async (id: string) => {
|
||||
const [_, deleteError] = await cartApi.delete(id);
|
||||
|
||||
if (deleteError) {
|
||||
console.error(deleteError);
|
||||
cancelCC()//мы хотели открыть модалку после покупки тарифа на создание квиза, но не вышло и модалку не откроем
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]") || [];
|
||||
if (!Array.isArray(saveCart)) saveCart = []
|
||||
saveCart = saveCart.push(id);
|
||||
localStorage.setItem("saveCart", JSON.stringify(saveCart));
|
||||
});
|
||||
}
|
||||
};
|
@ -6,7 +6,7 @@ import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
|
||||
import { Box, Button, IconButton, Popover, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { deleteQuiz, setEditQuizId } from "@root/quizes/actions";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { inCart } from "../../pages/Tariffs/Tariffs";
|
||||
import { inCart } from "../../pages/Tariffs/utils";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useDomainDefine } from "@utils/hooks/useDomainDefine";
|
||||
|
@ -1,9 +1,6 @@
|
||||
import {
|
||||
Box,
|
||||
FormControl,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
InputBase,
|
||||
SxProps,
|
||||
Theme,
|
||||
Typography,
|
||||
@ -17,23 +14,13 @@ import {
|
||||
useTicketStore,
|
||||
} from "@root/ticket";
|
||||
import type { TouchEvent, WheelEvent } from "react";
|
||||
import * as React from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import ChatMessage from "./ChatMessage";
|
||||
import ChatVideo from "./ChatVideo";
|
||||
import SendIcon from "@icons/SendIcon";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import ChatMessageRenderer from "./ChatMessageRenderer";
|
||||
import ChatInput from "./ChatInput";
|
||||
import UserCircleIcon from "./UserCircleIcon";
|
||||
import { throttle, TicketMessage } from "@frontend/kitui";
|
||||
import ArrowLeft from "@icons/questionsPage/arrowLeft";
|
||||
import { useUserStore } from "@root/user";
|
||||
import AttachFileIcon from "@mui/icons-material/AttachFile";
|
||||
import ChatImage from "./ChatImage";
|
||||
import ChatDocument from "@ui_kit/FloatingSupportChat/ChatDocument";
|
||||
import {
|
||||
ACCEPT_SEND_MEDIA_TYPES_MAP,
|
||||
checkAcceptableMediaType,
|
||||
} from "@utils/checkAcceptableMediaType";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
@ -41,22 +28,20 @@ interface Props {
|
||||
onclickArrow?: () => void;
|
||||
sendMessage: (a: string) => Promise<boolean>;
|
||||
sendFile: (a: File | undefined) => Promise<void>;
|
||||
greetingMessage: TicketMessage;
|
||||
}
|
||||
|
||||
const greetingMessage = "Здравствуйте, задайте ваш вопрос и наш оператор вам ответит в течение 10 минут";
|
||||
|
||||
export default function Chat({
|
||||
open = false,
|
||||
sx,
|
||||
onclickArrow,
|
||||
sendMessage,
|
||||
sendFile,
|
||||
greetingMessage,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(800));
|
||||
const [messageField, setMessageField] = useState<string>("");
|
||||
const [disableFileButton, setDisableFileButton] = useState(false);
|
||||
|
||||
const user = useUserStore((state) => state.user?._id);
|
||||
const ticket = useTicketStore(
|
||||
@ -72,31 +57,11 @@ export default function Chat({
|
||||
const chatBoxRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
addOrUpdateUnauthMessages([greetingMessage]);
|
||||
if (open) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const sendMessageHC = async () => {
|
||||
const successful = await sendMessage(messageField);
|
||||
if (successful) {
|
||||
setMessageField("");
|
||||
}
|
||||
};
|
||||
const sendFileHC = async (file: File) => {
|
||||
const check = checkAcceptableMediaType(file);
|
||||
if (check.length > 0) {
|
||||
enqueueSnackbar(check);
|
||||
return;
|
||||
}
|
||||
setDisableFileButton(true);
|
||||
await sendFile(file);
|
||||
setDisableFileButton(false);
|
||||
};
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const throttledScrollHandler = useMemo(
|
||||
() =>
|
||||
throttle(() => {
|
||||
@ -152,14 +117,6 @@ export default function Chat({
|
||||
behavior,
|
||||
});
|
||||
}
|
||||
const handleTextfieldKeyPress: React.KeyboardEventHandler<
|
||||
HTMLInputElement | HTMLTextAreaElement
|
||||
> = (e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessageHC();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -240,164 +197,34 @@ export default function Chat({
|
||||
>
|
||||
{ticket.sessionData?.ticketId &&
|
||||
messages.map((message) => {
|
||||
const isFileVideo = () => {
|
||||
if (message.files) {
|
||||
return ACCEPT_SEND_MEDIA_TYPES_MAP.video.some(
|
||||
(fileType) =>
|
||||
message.files[0].toLowerCase().endsWith(fileType),
|
||||
const isSelf = useMemo(() =>
|
||||
(ticket.sessionData?.sessionId || user) === message.user_id,
|
||||
[ticket.sessionData?.sessionId, user, message.user_id]
|
||||
);
|
||||
}
|
||||
};
|
||||
const isFileImage = () => {
|
||||
if (message.files) {
|
||||
return ACCEPT_SEND_MEDIA_TYPES_MAP.picture.some(
|
||||
(fileType) =>
|
||||
message.files[0].toLowerCase().endsWith(fileType),
|
||||
);
|
||||
}
|
||||
};
|
||||
const isFileDocument = () => {
|
||||
if (message.files) {
|
||||
return ACCEPT_SEND_MEDIA_TYPES_MAP.document.some(
|
||||
(fileType) =>
|
||||
message.files[0].toLowerCase().endsWith(fileType),
|
||||
);
|
||||
}
|
||||
};
|
||||
if (message.files.length > 0 && isFileImage()) {
|
||||
|
||||
return (
|
||||
<ChatImage
|
||||
unAuthenticated
|
||||
<ChatMessageRenderer
|
||||
key={message.id}
|
||||
file={message.files[0]}
|
||||
createdAt={message.created_at}
|
||||
isSelf={
|
||||
(ticket.sessionData?.sessionId || user) ===
|
||||
message.user_id
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (message.files.length > 0 && isFileVideo()) {
|
||||
return (
|
||||
<ChatVideo
|
||||
unAuthenticated
|
||||
key={message.id}
|
||||
file={message.files[0]}
|
||||
createdAt={message.created_at}
|
||||
isSelf={
|
||||
(ticket.sessionData?.sessionId || user) ===
|
||||
message.user_id
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (message.files.length > 0 && isFileDocument()) {
|
||||
return (
|
||||
<ChatDocument
|
||||
unAuthenticated
|
||||
key={message.id}
|
||||
file={message.files[0]}
|
||||
createdAt={message.created_at}
|
||||
isSelf={
|
||||
(ticket.sessionData?.sessionId || user) ===
|
||||
message.user_id
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ChatMessage
|
||||
unAuthenticated
|
||||
key={message.id}
|
||||
text={message.message}
|
||||
createdAt={message.created_at}
|
||||
isSelf={
|
||||
(ticket.sessionData?.sessionId || user) ===
|
||||
message.user_id
|
||||
}
|
||||
message={message}
|
||||
isSelf={isSelf}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{!ticket.sessionData?.ticketId && (
|
||||
<ChatMessage
|
||||
unAuthenticated
|
||||
text={greetingMessage.message}
|
||||
createdAt={greetingMessage.created_at}
|
||||
isSelf={
|
||||
(ticket.sessionData?.sessionId || user) ===
|
||||
greetingMessage.user_id
|
||||
}
|
||||
<ChatMessageRenderer
|
||||
message={greetingMessage}
|
||||
isSelf={useMemo(() =>
|
||||
(ticket.sessionData?.sessionId || user) === greetingMessage.user_id,
|
||||
[ticket.sessionData?.sessionId, user, greetingMessage.user_id]
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<FormControl fullWidth sx={{ borderTop: "1px solid black" }}>
|
||||
<InputBase
|
||||
value={messageField}
|
||||
fullWidth
|
||||
placeholder="Введите сообщение..."
|
||||
id="message"
|
||||
multiline
|
||||
onKeyDown={handleTextfieldKeyPress}
|
||||
sx={{
|
||||
width: "100%",
|
||||
p: 0,
|
||||
}}
|
||||
inputProps={{
|
||||
sx: {
|
||||
fontWeight: 400,
|
||||
fontSize: "16px",
|
||||
lineHeight: "19px",
|
||||
pt: upMd ? "30px" : "28px",
|
||||
pb: upMd ? "30px" : "24px",
|
||||
px: "19px",
|
||||
maxHeight: "calc(19px * 5)",
|
||||
color: "black",
|
||||
},
|
||||
}}
|
||||
onChange={(e) => setMessageField(e.target.value)}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
disabled={disableFileButton}
|
||||
onClick={() => {
|
||||
if (!disableFileButton) fileInputRef.current?.click();
|
||||
}}
|
||||
>
|
||||
<AttachFileIcon />
|
||||
</IconButton>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
id="fileinput"
|
||||
onChange={(e) => {
|
||||
if (e.target.files?.[0])
|
||||
sendFileHC(e.target.files?.[0]);
|
||||
}}
|
||||
style={{ display: "none" }}
|
||||
type="file"
|
||||
<ChatInput
|
||||
sendMessage={sendMessage}
|
||||
sendFile={sendFile}
|
||||
isMessageSending={isMessageSending}
|
||||
/>
|
||||
<IconButton
|
||||
disabled={isMessageSending}
|
||||
onClick={sendMessageHC}
|
||||
sx={{
|
||||
height: "53px",
|
||||
width: "53px",
|
||||
mr: "13px",
|
||||
p: 0,
|
||||
opacity: isMessageSending ? 0.3 : 1,
|
||||
}}
|
||||
>
|
||||
<SendIcon
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
137
src/ui_kit/FloatingSupportChat/ChatInput.tsx
Normal file
137
src/ui_kit/FloatingSupportChat/ChatInput.tsx
Normal file
@ -0,0 +1,137 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import {
|
||||
FormControl,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
InputBase,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import SendIcon from "@icons/SendIcon";
|
||||
import AttachFileIcon from "@mui/icons-material/AttachFile";
|
||||
import { checkAcceptableMediaType } from "@utils/checkAcceptableMediaType";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
|
||||
interface ChatInputProps {
|
||||
sendMessage: (message: string) => Promise<boolean>;
|
||||
sendFile: (file: File | undefined) => Promise<void>;
|
||||
isMessageSending: boolean;
|
||||
}
|
||||
|
||||
const ChatInput = ({ sendMessage, sendFile, isMessageSending }: ChatInputProps) => {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const [messageField, setMessageField] = useState<string>("");
|
||||
const [disableFileButton, setDisableFileButton] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleSendMessage = useCallback(async () => {
|
||||
const successful = await sendMessage(messageField);
|
||||
if (successful) {
|
||||
setMessageField("");
|
||||
}
|
||||
}, [sendMessage, messageField]);
|
||||
|
||||
const handleSendFile = useCallback(async (file: File) => {
|
||||
const check = checkAcceptableMediaType(file);
|
||||
if (check.length > 0) {
|
||||
enqueueSnackbar(check);
|
||||
return;
|
||||
}
|
||||
setDisableFileButton(true);
|
||||
await sendFile(file);
|
||||
setDisableFileButton(false);
|
||||
}, [sendFile]);
|
||||
|
||||
const handleFileInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files?.[0]) {
|
||||
handleSendFile(e.target.files[0]);
|
||||
}
|
||||
}, [handleSendFile]);
|
||||
|
||||
const handleFileButtonClick = useCallback(() => {
|
||||
if (!disableFileButton) {
|
||||
fileInputRef.current?.click();
|
||||
}
|
||||
}, [disableFileButton]);
|
||||
|
||||
const handleTextfieldKeyPress: React.KeyboardEventHandler<
|
||||
HTMLInputElement | HTMLTextAreaElement
|
||||
> = useCallback((e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
}, [handleSendMessage]);
|
||||
|
||||
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setMessageField(e.target.value);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FormControl fullWidth sx={{ borderTop: "1px solid black" }}>
|
||||
<InputBase
|
||||
value={messageField}
|
||||
fullWidth
|
||||
placeholder="Введите сообщение..."
|
||||
id="message"
|
||||
multiline
|
||||
onKeyDown={handleTextfieldKeyPress}
|
||||
sx={{
|
||||
width: "100%",
|
||||
p: 0,
|
||||
}}
|
||||
inputProps={{
|
||||
sx: {
|
||||
fontWeight: 400,
|
||||
fontSize: "16px",
|
||||
lineHeight: "19px",
|
||||
pt: upMd ? "30px" : "28px",
|
||||
pb: upMd ? "30px" : "24px",
|
||||
px: "19px",
|
||||
maxHeight: "calc(19px * 5)",
|
||||
color: "black",
|
||||
},
|
||||
}}
|
||||
onChange={handleInputChange}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
disabled={disableFileButton}
|
||||
onClick={handleFileButtonClick}
|
||||
>
|
||||
<AttachFileIcon />
|
||||
</IconButton>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
id="fileinput"
|
||||
onChange={handleFileInputChange}
|
||||
style={{ display: "none" }}
|
||||
type="file"
|
||||
/>
|
||||
<IconButton
|
||||
disabled={isMessageSending}
|
||||
onClick={handleSendMessage}
|
||||
sx={{
|
||||
height: "53px",
|
||||
width: "53px",
|
||||
mr: "13px",
|
||||
p: 0,
|
||||
opacity: isMessageSending ? 0.3 : 1,
|
||||
}}
|
||||
>
|
||||
<SendIcon
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatInput;
|
70
src/ui_kit/FloatingSupportChat/ChatMessageRenderer.tsx
Normal file
70
src/ui_kit/FloatingSupportChat/ChatMessageRenderer.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { memo, useMemo } from "react";
|
||||
import { TicketMessage } from "@frontend/kitui";
|
||||
import ChatMessage from "./ChatMessage";
|
||||
import ChatImage from "./ChatImage";
|
||||
import ChatVideo from "./ChatVideo";
|
||||
import ChatDocument from "./ChatDocument";
|
||||
import { ACCEPT_SEND_MEDIA_TYPES_MAP } from "@utils/checkAcceptableMediaType";
|
||||
|
||||
interface ChatMessageRendererProps {
|
||||
message: TicketMessage;
|
||||
isSelf: boolean;
|
||||
}
|
||||
|
||||
const ChatMessageRenderer = memo(({ message, isSelf }: ChatMessageRendererProps) => {
|
||||
const fileType = useMemo(() => {
|
||||
if (!message.files?.length) return null;
|
||||
|
||||
const fileName = message.files[0].toLowerCase();
|
||||
|
||||
if (ACCEPT_SEND_MEDIA_TYPES_MAP.video.some(fileType => fileName.endsWith(fileType))) {
|
||||
return 'video';
|
||||
}
|
||||
|
||||
if (ACCEPT_SEND_MEDIA_TYPES_MAP.picture.some(fileType => fileName.endsWith(fileType))) {
|
||||
return 'image';
|
||||
}
|
||||
|
||||
if (ACCEPT_SEND_MEDIA_TYPES_MAP.document.some(fileType => fileName.endsWith(fileType))) {
|
||||
return 'document';
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [message.files]);
|
||||
|
||||
// Если есть файлы и определён тип
|
||||
if (message.files?.length > 0 && fileType) {
|
||||
const commonProps = {
|
||||
unAuthenticated: true,
|
||||
key: message.id,
|
||||
file: message.files[0],
|
||||
createdAt: message.created_at,
|
||||
isSelf,
|
||||
};
|
||||
|
||||
switch (fileType) {
|
||||
case 'image':
|
||||
return <ChatImage {...commonProps} />;
|
||||
case 'video':
|
||||
return <ChatVideo {...commonProps} />;
|
||||
case 'document':
|
||||
return <ChatDocument {...commonProps} />;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Текстовое сообщение
|
||||
return (
|
||||
<ChatMessage
|
||||
unAuthenticated
|
||||
text={message.message}
|
||||
createdAt={message.created_at}
|
||||
isSelf={isSelf}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ChatMessageRenderer.displayName = 'ChatMessageRenderer';
|
||||
|
||||
export default ChatMessageRenderer;
|
@ -47,7 +47,6 @@ interface Props {
|
||||
sendFile: (a: File | undefined) => Promise<void>;
|
||||
modalWarningType: string | null;
|
||||
setModalWarningType: any;
|
||||
greetingMessage: TicketMessage;
|
||||
}
|
||||
|
||||
export default function FloatingSupportChat({
|
||||
@ -59,7 +58,6 @@ export default function FloatingSupportChat({
|
||||
sendFile,
|
||||
modalWarningType,
|
||||
setModalWarningType,
|
||||
greetingMessage,
|
||||
}: Props) {
|
||||
const [monitorType, setMonitorType] = useState<"desktop" | "mobile" | "">("");
|
||||
const theme = useTheme();
|
||||
@ -108,7 +106,6 @@ export default function FloatingSupportChat({
|
||||
sx={{ alignSelf: "start", width: "clamp(200px, 100%, 400px)" }}
|
||||
sendMessage={sendMessage}
|
||||
sendFile={sendFile}
|
||||
greetingMessage={greetingMessage}
|
||||
/>
|
||||
<Dialog
|
||||
fullScreen
|
||||
@ -121,7 +118,6 @@ export default function FloatingSupportChat({
|
||||
onclickArrow={handleChatClickClose}
|
||||
sendMessage={sendMessage}
|
||||
sendFile={sendFile}
|
||||
greetingMessage={greetingMessage}
|
||||
/>
|
||||
</Dialog>
|
||||
<Fab
|
||||
|
@ -72,33 +72,6 @@ export default () => {
|
||||
setIsChatOpened((state) => !state);
|
||||
};
|
||||
|
||||
const getGreetingMessage: TicketMessage = useMemo(() => {
|
||||
const workingHoursMessage =
|
||||
"Здравствуйте, задайте ваш вопрос и наш оператор вам ответит в течение 10 минут";
|
||||
const offHoursMessage =
|
||||
"Здравствуйте, задайте ваш вопрос и наш оператор вам ответит в течение 10 минут";
|
||||
const date = new Date();
|
||||
const currentHourUTC = date.getUTCHours();
|
||||
const MscTime = 3; // Москва UTC+3;
|
||||
const moscowHour = (currentHourUTC + MscTime) % 24;
|
||||
const greetingMessage =
|
||||
moscowHour >= 3 && moscowHour < 10
|
||||
? offHoursMessage
|
||||
: workingHoursMessage;
|
||||
|
||||
return {
|
||||
created_at: new Date().toISOString(),
|
||||
files: [],
|
||||
id: "111",
|
||||
message: greetingMessage,
|
||||
request_screenshot: "",
|
||||
session_id: "greetingMessage",
|
||||
shown: { me: 1 },
|
||||
ticket_id: "111",
|
||||
user_id: "greetingMessage",
|
||||
};
|
||||
}, [isChatOpened]);
|
||||
|
||||
useTicketsFetcher({
|
||||
url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/getTickets`,
|
||||
ticketsPerPage: 10,
|
||||
@ -157,7 +130,6 @@ export default () => {
|
||||
);
|
||||
if (isTicketClosed) {
|
||||
cleanAuthTicketData();
|
||||
addOrUpdateUnauthMessages([getGreetingMessage]);
|
||||
if (!user) {
|
||||
cleanUnauthTicketData();
|
||||
localStorage.removeItem("unauth-ticket");
|
||||
@ -185,8 +157,8 @@ export default () => {
|
||||
({ shown }) => shown?.me !== 1,
|
||||
);
|
||||
|
||||
newMessages.map(async ({ id }) => {
|
||||
await shownMessage(id);
|
||||
newMessages.forEach(({ id, user_id }) => {
|
||||
if ((ticket.sessionData?.sessionId || user) === user_id) shownMessage(id);
|
||||
});
|
||||
}
|
||||
}, [isChatOpened, ticket.messages]);
|
||||
@ -248,7 +220,6 @@ export default () => {
|
||||
sendFile={sendFile}
|
||||
modalWarningType={modalWarningType}
|
||||
setModalWarningType={setModalWarningType}
|
||||
greetingMessage={getGreetingMessage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,274 +0,0 @@
|
||||
import { useSSETab } from "@/utils/hooks/useSSETab";
|
||||
import { parseAxiosError } from "@/utils/parse-error";
|
||||
import { TicketMessage, createTicket, useSSESubscription, useTicketMessages, useTicketsFetcher, sendFile as sf, sendTicketMessage, shownMessage } from "@frontend/kitui";
|
||||
|
||||
import {
|
||||
addOrUpdateUnauthMessages,
|
||||
cleanAuthTicketData,
|
||||
cleanUnauthTicketData,
|
||||
setIsMessageSending,
|
||||
setTicketData,
|
||||
setUnauthIsPreventAutoscroll,
|
||||
setUnauthTicketMessageFetchState,
|
||||
useTicketStore,
|
||||
} from "@root/ticket";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
userId?: string;
|
||||
|
||||
}
|
||||
|
||||
type ModalWarningType =
|
||||
| "errorType"
|
||||
| "errorSize"
|
||||
| "picture"
|
||||
| "video"
|
||||
| "audio"
|
||||
| "document"
|
||||
| null;
|
||||
const MAX_FILE_SIZE = 419430400;
|
||||
const ACCEPT_SEND_FILE_TYPES_MAP = [
|
||||
".jpeg",
|
||||
".jpg",
|
||||
".png",
|
||||
".mp4",
|
||||
".doc",
|
||||
".docx",
|
||||
".pdf",
|
||||
".txt",
|
||||
".xlsx",
|
||||
".csv",
|
||||
] as const;
|
||||
export default ({ userId }: Props) => {
|
||||
const ticket = useTicketStore((state) => state[userId ? "authData" : "unauthData"]);
|
||||
|
||||
const { isActiveSSETab, updateSSEValue } = useSSETab<TicketMessage[]>(
|
||||
"ticket",
|
||||
addOrUpdateUnauthMessages,
|
||||
);
|
||||
|
||||
const [modalWarningType, setModalWarningType] =
|
||||
useState<ModalWarningType>(null);
|
||||
const [isChatOpened, setIsChatOpened] = useState<boolean>(false);
|
||||
const [sseEnabled, setSseEnabled] = useState(true);
|
||||
|
||||
const handleChatClickOpen = () => {
|
||||
setIsChatOpened(true);
|
||||
};
|
||||
const handleChatClickClose = () => {
|
||||
setIsChatOpened(false);
|
||||
};
|
||||
const handleChatClickSwitch = () => {
|
||||
setIsChatOpened((state) => !state);
|
||||
};
|
||||
|
||||
const getGreetingMessage: TicketMessage = useMemo(() => {
|
||||
const workingHoursMessage =
|
||||
"Здравствуйте, задайте ваш вопрос и наш оператор вам ответит в течение 10 минут";
|
||||
const offHoursMessage =
|
||||
"Здравствуйте, задайте ваш вопрос и наш оператор вам ответит в течение 10 минут";
|
||||
const date = new Date();
|
||||
const currentHourUTC = date.getUTCHours();
|
||||
const MscTime = 3; // Москва UTC+3;
|
||||
const moscowHour = (currentHourUTC + MscTime) % 24;
|
||||
const greetingMessage =
|
||||
moscowHour >= 3 && moscowHour < 10
|
||||
? offHoursMessage
|
||||
: workingHoursMessage;
|
||||
|
||||
return {
|
||||
created_at: new Date().toISOString(),
|
||||
files: [],
|
||||
id: "111",
|
||||
message: greetingMessage,
|
||||
request_screenshot: "",
|
||||
session_id: "greetingMessage",
|
||||
shown: { me: 1 },
|
||||
ticket_id: "111",
|
||||
user_id: "greetingMessage",
|
||||
};
|
||||
}, [isChatOpened]);
|
||||
|
||||
useTicketsFetcher({
|
||||
url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/getTickets`,
|
||||
ticketsPerPage: 10,
|
||||
ticketApiPage: 0,
|
||||
onSuccess: (result) => {
|
||||
if (result.data?.length) {
|
||||
const currentTicket = result.data.find(
|
||||
({ origin }) => !origin.includes("/support"),
|
||||
);
|
||||
|
||||
if (!currentTicket) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTicketData({
|
||||
ticketId: currentTicket.id,
|
||||
sessionId: currentTicket.sess,
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
const message = parseAxiosError(error);
|
||||
if (message) enqueueSnackbar(message);
|
||||
},
|
||||
onFetchStateChange: () => { },
|
||||
enabled: Boolean(userId),
|
||||
});
|
||||
|
||||
useTicketMessages({
|
||||
url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/getMessages`,
|
||||
isUnauth: true,
|
||||
ticketId: ticket.sessionData?.ticketId,
|
||||
messagesPerPage: ticket.messagesPerPage,
|
||||
messageApiPage: ticket.apiPage,
|
||||
onSuccess: useCallback((messages) => {
|
||||
addOrUpdateUnauthMessages(messages);
|
||||
}, []),
|
||||
onError: useCallback((error: Error) => {
|
||||
if (error.name === "CanceledError") {
|
||||
return;
|
||||
}
|
||||
|
||||
const [message] = parseAxiosError(error);
|
||||
if (message) enqueueSnackbar(message);
|
||||
}, []),
|
||||
onFetchStateChange: setUnauthTicketMessageFetchState,
|
||||
});
|
||||
|
||||
useSSESubscription<TicketMessage>({
|
||||
enabled:
|
||||
sseEnabled && isActiveSSETab && Boolean(ticket.sessionData?.sessionId),
|
||||
url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/ticket?ticket=${ticket.sessionData?.ticketId}&s=${ticket.sessionData?.sessionId}`,
|
||||
onNewData: (ticketMessages) => {
|
||||
const isTicketClosed = ticketMessages.some(
|
||||
(message) => message.session_id === "close",
|
||||
);
|
||||
if (isTicketClosed) {
|
||||
cleanAuthTicketData();
|
||||
addOrUpdateUnauthMessages([getGreetingMessage]);
|
||||
if (!userId) {
|
||||
cleanUnauthTicketData();
|
||||
localStorage.removeItem("unauth-ticket");
|
||||
}
|
||||
return;
|
||||
}
|
||||
updateSSEValue(ticketMessages);
|
||||
addOrUpdateUnauthMessages(ticketMessages);
|
||||
},
|
||||
onDisconnect: useCallback(() => {
|
||||
setUnauthIsPreventAutoscroll(false);
|
||||
setSseEnabled(false);
|
||||
}, []),
|
||||
marker: "ticket",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
cleanAuthTicketData();
|
||||
setSseEnabled(true);
|
||||
}, [userId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isChatOpened) {
|
||||
const newMessages = ticket.messages.filter(
|
||||
({ shown }) => shown?.me !== 1,
|
||||
);
|
||||
|
||||
newMessages.map(async ({ id }) => {
|
||||
await shownMessage(id);
|
||||
});
|
||||
}
|
||||
}, [isChatOpened, ticket.messages]);
|
||||
|
||||
const sendMessage = async (messageField: string) => {
|
||||
if (!messageField || ticket.isMessageSending) return false;
|
||||
setSseEnabled(true);
|
||||
let successful = false;
|
||||
setIsMessageSending(true);
|
||||
if (!ticket.sessionData?.ticketId) {
|
||||
const [data, createError] = await createTicket({
|
||||
message: messageField,
|
||||
useToken: Boolean(userId),
|
||||
systemError: false
|
||||
});
|
||||
|
||||
if (createError || !data) {
|
||||
successful = false;
|
||||
|
||||
enqueueSnackbar(`Не удалось создать чат ${(createError)}`);
|
||||
} else {
|
||||
successful = true;
|
||||
|
||||
setTicketData({ ticketId: data.Ticket, sessionId: data.sess });
|
||||
}
|
||||
|
||||
setIsMessageSending(false);
|
||||
} else {
|
||||
const [_, sendTicketMessageError] = await sendTicketMessage({
|
||||
ticketId: ticket.sessionData?.ticketId,
|
||||
message: messageField,
|
||||
systemError: false
|
||||
});
|
||||
successful = true;
|
||||
|
||||
if (sendTicketMessageError) {
|
||||
successful = false;
|
||||
enqueueSnackbar(`Ошибка отправки сообщения ${parseAxiosError(sendTicketMessageError)}`);
|
||||
}
|
||||
setIsMessageSending(false);
|
||||
}
|
||||
|
||||
return successful;
|
||||
};
|
||||
const sendFile = async (file: File) => {
|
||||
if (file === undefined) return true;
|
||||
|
||||
let ticketId = ticket.sessionData?.ticketId;
|
||||
if (!ticket.sessionData?.ticketId) {
|
||||
const [data, createError] = await createTicket({
|
||||
message: "",
|
||||
useToken: Boolean(userId),
|
||||
systemError: false
|
||||
});
|
||||
ticketId = data?.Ticket;
|
||||
|
||||
if (createError || !data) {
|
||||
enqueueSnackbar(`Не удалось создать диалог ${parseAxiosError(createError)}`);
|
||||
} else {
|
||||
setTicketData({ ticketId: data.Ticket, sessionId: data.sess });
|
||||
}
|
||||
|
||||
setIsMessageSending(false);
|
||||
}
|
||||
|
||||
if (ticketId !== undefined) {
|
||||
if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize");
|
||||
|
||||
const [_, sendFileError] = await sf({
|
||||
ticketId,
|
||||
file
|
||||
});
|
||||
|
||||
if (sendFileError) {
|
||||
enqueueSnackbar(sendFileError);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
isChatOpened,
|
||||
handleChatClickOpen,
|
||||
handleChatClickClose,
|
||||
handleChatClickSwitch,
|
||||
sendMessage,
|
||||
sendFile,
|
||||
modalWarningType,
|
||||
setModalWarningType,
|
||||
getGreetingMessage
|
||||
};
|
||||
};
|
@ -4,7 +4,7 @@ import { useSSETab } from "./useSSETab";
|
||||
import { cancelPayCartProcess } from "@/stores/notEnoughMoneyAmount";
|
||||
import { setCash } from "@/stores/cash";
|
||||
import { currencyFormatter } from "@/pages/Tariffs/tariffsUtils/currencyFormatter";
|
||||
import { inCart } from "@/pages/Tariffs/Tariffs";
|
||||
import { inCart } from "@/pages/Tariffs/utils";
|
||||
|
||||
type Ping = [{ event: "ping" }]
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user