diff --git a/src/pages/PersonalizationAI/PersonalizationAI.tsx b/src/pages/PersonalizationAI/PersonalizationAI.tsx index d86fede9..0aacd104 100644 --- a/src/pages/PersonalizationAI/PersonalizationAI.tsx +++ b/src/pages/PersonalizationAI/PersonalizationAI.tsx @@ -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"; diff --git a/src/pages/Tariffs/TariffCardDisplaySelector.tsx b/src/pages/Tariffs/TariffCardDisplaySelector.tsx new file mode 100644 index 00000000..617febfa --- /dev/null +++ b/src/pages/Tariffs/TariffCardDisplaySelector.tsx @@ -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 + {content.map(data => )} + + + 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, + + ) + + 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 + {content.map(data => )} + + } +} \ No newline at end of file diff --git a/src/pages/Tariffs/Tariffs.tsx b/src/pages/Tariffs/Tariffs.tsx index 5a886bb0..ed8fc735 100644 --- a/src/pages/Tariffs/Tariffs.tsx +++ b/src/pages/Tariffs/Tariffs.tsx @@ -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 = { 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 ( <> - - - - - navigate("/list")}> - - - - - - Мой баланс - - 9 ? "13px" : "16px") : "16px" - } - > - {cashString} - - - { - navigate("/"); - handleLogoutClick(); - }} - sx={{ - ml: "20px", - }} - /> - - + setSelectedItem("create") }, + { + title: "Премиум функции", + onClick: () => setSelectedItem("premium") + }, + { + title: "Расширенная аналитика", + onClick: () => setSelectedItem("analytics") + }, + { + title: "Кастомные тарифы", + onClick: () => setSelectedItem("custom") + }, ]} tariffs={tariffs} @@ -304,38 +257,60 @@ console.log(tariffs) startRequestCreate={startRequestCreate} /> )} + {selectedItem === "dop" && ( + 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} + openModalHC={openModalHC} + userPrivilegies={userPrivilegies} + startRequestCreate={startRequestCreate} + /> + )} - 0} onClose={() => setOpenModal({})} - > - - - Вы подтверждаете платёж в сумму{" "} - {openModal.price ? openModal.price.toFixed(2) : 0} ₽ - - - - + onConfirm={() => tryBuy(openModal)} + price={openModal.price} + /> setIsRequestCreate(false)} /> ); @@ -364,47 +339,3 @@ const LoadingPage = () => ( ); - -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)); - }); - } - -}; diff --git a/src/pages/Tariffs/components/PaymentConfirmationModal.tsx b/src/pages/Tariffs/components/PaymentConfirmationModal.tsx new file mode 100644 index 00000000..0d13eb46 --- /dev/null +++ b/src/pages/Tariffs/components/PaymentConfirmationModal.tsx @@ -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 ( + + + + Вы подтверждаете платёж в сумму{" "} + {price ? price.toFixed(2) : 0} ₽ + + + + + ); +}; \ No newline at end of file diff --git a/src/pages/Tariffs/components/TariffsHeader.tsx b/src/pages/Tariffs/components/TariffsHeader.tsx new file mode 100644 index 00000000..63965510 --- /dev/null +++ b/src/pages/Tariffs/components/TariffsHeader.tsx @@ -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 ( + + + + + navigate("/list")}> + + + + + + Мой баланс + + 9 ? "13px" : "16px") : "16px" + } + > + {cashString} + + + { + navigate("/"); + handleLogoutClick(); + }} + sx={{ + ml: "20px", + }} + /> + + + ); +}; \ No newline at end of file diff --git a/src/pages/Tariffs/pages/HideLogo.tsx b/src/pages/Tariffs/pages/HideLogo.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/src/pages/Tariffs/pages/Other.tsx b/src/pages/Tariffs/pages/Other.tsx deleted file mode 100644 index 5373da51..00000000 --- a/src/pages/Tariffs/pages/Other.tsx +++ /dev/null @@ -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 - {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, - - )} - - default: - return - {content.map(data => )} - - } -} diff --git a/src/pages/Tariffs/types.ts b/src/pages/Tariffs/types.ts index fd6c0dea..6db0e8d6 100644 --- a/src/pages/Tariffs/types.ts +++ b/src/pages/Tariffs/types.ts @@ -1 +1 @@ -type TypePages = "count" | "day" | "dop" | "hide" | "create" \ No newline at end of file +type TypePages = "count" | "day" | "dop" | "hide" | "create" | "premium" | "analytics" | "custom" \ No newline at end of file diff --git a/src/pages/Tariffs/utils.ts b/src/pages/Tariffs/utils.ts new file mode 100644 index 00000000..ca612a35 --- /dev/null +++ b/src/pages/Tariffs/utils.ts @@ -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)); + }); + } +}; \ No newline at end of file diff --git a/src/pages/createQuize/QuizCard.tsx b/src/pages/createQuize/QuizCard.tsx index dabf007a..9ffc4d44 100755 --- a/src/pages/createQuize/QuizCard.tsx +++ b/src/pages/createQuize/QuizCard.tsx @@ -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"; diff --git a/src/ui_kit/FloatingSupportChat/Chat.tsx b/src/ui_kit/FloatingSupportChat/Chat.tsx index 7ce80acd..ab7f231f 100644 --- a/src/ui_kit/FloatingSupportChat/Chat.tsx +++ b/src/ui_kit/FloatingSupportChat/Chat.tsx @@ -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; sendFile: (a: File | undefined) => Promise; - 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(""); - 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(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(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 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 ( - - ); - } - if (message.files.length > 0 && isFileVideo()) { - return ( - - ); - } - if (message.files.length > 0 && isFileDocument()) { - return ( - - ); - } + const isSelf = useMemo(() => + (ticket.sessionData?.sessionId || user) === message.user_id, + [ticket.sessionData?.sessionId, user, message.user_id] + ); + return ( - ); })} {!ticket.sessionData?.ticketId && ( - + (ticket.sessionData?.sessionId || user) === greetingMessage.user_id, + [ticket.sessionData?.sessionId, user, greetingMessage.user_id] + )} /> )} - - setMessageField(e.target.value)} - endAdornment={ - - { - if (!disableFileButton) fileInputRef.current?.click(); - }} - > - - - { - if (e.target.files?.[0]) - sendFileHC(e.target.files?.[0]); - }} - style={{ display: "none" }} - type="file" - /> - - - - - } - /> - + )} diff --git a/src/ui_kit/FloatingSupportChat/ChatInput.tsx b/src/ui_kit/FloatingSupportChat/ChatInput.tsx new file mode 100644 index 00000000..ba17b903 --- /dev/null +++ b/src/ui_kit/FloatingSupportChat/ChatInput.tsx @@ -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; + sendFile: (file: File | undefined) => Promise; + isMessageSending: boolean; +} + +const ChatInput = ({ sendMessage, sendFile, isMessageSending }: ChatInputProps) => { + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const [messageField, setMessageField] = useState(""); + const [disableFileButton, setDisableFileButton] = useState(false); + const fileInputRef = useRef(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) => { + 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) => { + setMessageField(e.target.value); + }, []); + + return ( + + + + + + + + + + + } + /> + + ); +}; + +export default ChatInput; \ No newline at end of file diff --git a/src/ui_kit/FloatingSupportChat/ChatMessageRenderer.tsx b/src/ui_kit/FloatingSupportChat/ChatMessageRenderer.tsx new file mode 100644 index 00000000..4955915e --- /dev/null +++ b/src/ui_kit/FloatingSupportChat/ChatMessageRenderer.tsx @@ -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 ; + case 'video': + return ; + case 'document': + return ; + default: + break; + } + } + + // Текстовое сообщение + return ( + + ); +}); + +ChatMessageRenderer.displayName = 'ChatMessageRenderer'; + +export default ChatMessageRenderer; \ No newline at end of file diff --git a/src/ui_kit/FloatingSupportChat/FloatingSupportChat.tsx b/src/ui_kit/FloatingSupportChat/FloatingSupportChat.tsx index 1ee0ba3e..2c7f7419 100644 --- a/src/ui_kit/FloatingSupportChat/FloatingSupportChat.tsx +++ b/src/ui_kit/FloatingSupportChat/FloatingSupportChat.tsx @@ -47,7 +47,6 @@ interface Props { sendFile: (a: File | undefined) => Promise; 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} /> { 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} /> ); }; diff --git a/src/ui_kit/FloatingSupportChat/useTechnicalSupport.ts b/src/ui_kit/FloatingSupportChat/useTechnicalSupport.ts deleted file mode 100644 index e9f42cbe..00000000 --- a/src/ui_kit/FloatingSupportChat/useTechnicalSupport.ts +++ /dev/null @@ -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( - "ticket", - addOrUpdateUnauthMessages, - ); - - const [modalWarningType, setModalWarningType] = - useState(null); - const [isChatOpened, setIsChatOpened] = useState(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({ - 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 - }; -}; \ No newline at end of file diff --git a/src/utils/hooks/usePipeSubscriber.ts b/src/utils/hooks/usePipeSubscriber.ts index 3c450e6b..8a9c2303 100644 --- a/src/utils/hooks/usePipeSubscriber.ts +++ b/src/utils/hooks/usePipeSubscriber.ts @@ -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" }]