From 801df79eba893727ec0a8f5e6d861b07d03a9721 Mon Sep 17 00:00:00 2001 From: IlyaDoronin Date: Thu, 31 Aug 2023 13:02:11 +0300 Subject: [PATCH] refactor: server requests --- src/api/auth.ts | 57 +- src/api/price.ts | 29 + src/api/tariff.ts | 46 ++ src/api/ticket.ts | 50 ++ src/api/user.ts | 6 +- src/api/verification.ts | 8 +- src/components/FloatingSupportChat/Chat.tsx | 508 +++++++++-------- src/components/ProtectedLayout.tsx | 9 +- src/model/auth.ts | 2 +- src/pages/Support/SupportChat.tsx | 589 ++++++++++---------- src/pages/auth/Signin.tsx | 350 ++++++------ src/pages/auth/Signup.tsx | 390 ++++++------- src/utils/hooks/useCustomTariffs.ts | 56 +- src/utils/hooks/useDiscounts.ts | 56 +- src/utils/hooks/useTariffFetcher.ts | 75 ++- 15 files changed, 1232 insertions(+), 999 deletions(-) create mode 100644 src/api/price.ts create mode 100644 src/api/ticket.ts diff --git a/src/api/auth.ts b/src/api/auth.ts index 13cad6f..eabe5c5 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -2,13 +2,66 @@ import { makeRequest } from "@frontend/kitui"; import { parseAxiosError } from "@root/utils/parse-error"; +import type { + LoginRequest, + LoginResponse, + RegisterRequest, + RegisterResponse, +} from "@frontend/kitui"; + const apiUrl = - process.env.NODE_ENV === "production" ? "" : "https://hub.pena.digital"; + process.env.NODE_ENV === "production" + ? "/auth" + : "https://hub.pena.digital/auth"; + +export async function register( + login: string, + password: string, + phoneNumber: string +): Promise<[RegisterResponse | null, string?]> { + try { + const registerResponse = await makeRequest< + RegisterRequest, + RegisterResponse + >({ + url: apiUrl + "/register", + body: { login, password, phoneNumber }, + useToken: false, + withCredentials: true, + }); + + return [registerResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось зарегестрировать аккаунт. ${error}`]; + } +} + +export async function login( + login: string, + password: string +): Promise<[LoginResponse | null, string?]> { + try { + const loginResponse = await makeRequest({ + url: apiUrl + "/login", + body: { login, password }, + useToken: false, + withCredentials: true, + }); + + return [loginResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось войти. ${error}`]; + } +} export async function logout(): Promise<[unknown, string?]> { try { const logoutResponse = await makeRequest({ - url: apiUrl + "/auth/logout", + url: apiUrl + "/logout", method: "POST", useToken: true, withCredentials: true, diff --git a/src/api/price.ts b/src/api/price.ts new file mode 100644 index 0000000..f6d2019 --- /dev/null +++ b/src/api/price.ts @@ -0,0 +1,29 @@ +import { makeRequest } from "@frontend/kitui"; + +import { parseAxiosError } from "@root/utils/parse-error"; + +import type { GetDiscountsResponse } from "@root/model/discount"; + +const apiUrl = + process.env.NODE_ENV === "production" + ? "/price" + : "https://hub.pena.digital/price"; + +export async function getDiscounts( + signal: AbortSignal | undefined +): Promise<[GetDiscountsResponse | null, string?]> { + try { + const discountsResponse = await makeRequest({ + url: apiUrl + "/discounts", + method: "get", + useToken: true, + signal, + }); + + return [discountsResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Ошибка получения списка скидок. ${error}`]; + } +} diff --git a/src/api/tariff.ts b/src/api/tariff.ts index e1589cb..ab97f02 100644 --- a/src/api/tariff.ts +++ b/src/api/tariff.ts @@ -2,11 +2,35 @@ import { Tariff, makeRequest } from "@frontend/kitui"; import { CreateTariffBody } from "@root/model/customTariffs"; import { parseAxiosError } from "@root/utils/parse-error"; +import type { ServiceKeyToPrivilegesMap } from "@root/model/privilege"; +import type { GetTariffsResponse } from "@root/model/tariff"; + const apiUrl = process.env.NODE_ENV === "production" ? "/strator" : "https://hub.pena.digital/strator"; +export async function getTariffs( + apiPage: number, + tariffsPerPage: number, + signal: AbortSignal | undefined +): Promise<[GetTariffsResponse | null, string?]> { + try { + const tariffsResponse = await makeRequest({ + url: apiUrl + `/tariff?page=${apiPage}&limit=${tariffsPerPage}`, + method: "get", + useToken: true, + signal, + }); + + return [tariffsResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось получить список тарифов. ${error}`]; + } +} + export async function createTariff( tariff: CreateTariffBody ): Promise<[Tariff | null, string?]> { @@ -43,3 +67,25 @@ export async function getTariffById( return [null, `Не удалось получить тарифы. ${error}`, status]; } } + +export async function getCustomTariffs( + signal: AbortSignal | undefined +): Promise<[ServiceKeyToPrivilegesMap | null, string?]> { + try { + const getCustomTariffsResponse = await makeRequest< + null, + ServiceKeyToPrivilegesMap + >({ + url: apiUrl + "/privilege/service", + signal, + method: "get", + useToken: true, + }); + + return [getCustomTariffsResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось получить кастомные тарифы. ${error}`]; + } +} diff --git a/src/api/ticket.ts b/src/api/ticket.ts new file mode 100644 index 0000000..c9b298e --- /dev/null +++ b/src/api/ticket.ts @@ -0,0 +1,50 @@ +import { makeRequest } from "@frontend/kitui"; +import { parseAxiosError } from "@root/utils/parse-error"; + +import { SendTicketMessageRequest } from "@frontend/kitui"; + +const apiUrl = + process.env.NODE_ENV === "production" + ? "/heruvym" + : "https://hub.pena.digital/heruvym"; + +export async function sendTicketMessage( + ticketId: string, + message: string +): Promise<[null, string?]> { + try { + const sendTicketMessageResponse = await makeRequest< + SendTicketMessageRequest, + null + >({ + url: `${apiUrl}/send`, + method: "POST", + useToken: false, + body: { ticket: ticketId, message: message, lang: "ru", files: [] }, + withCredentials: true, + }); + + return [sendTicketMessageResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось отправить сообщение. ${error}`]; + } +} + +export async function shownMessage(id: string): Promise<[null, string?]> { + try { + const shownMessageResponse = await makeRequest<{ id: string }, null>({ + url: apiUrl + "/shown", + method: "POST", + useToken: true, + body: { id }, + }); + + return [shownMessageResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось прочесть сообщение. ${error}`]; + } +} diff --git a/src/api/user.ts b/src/api/user.ts index 4f5bcf8..b000856 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -3,14 +3,16 @@ import { PatchUserRequest } from "@root/model/user"; import { parseAxiosError } from "@root/utils/parse-error"; const apiUrl = - process.env.NODE_ENV === "production" ? "" : "https://hub.pena.digital"; + process.env.NODE_ENV === "production" + ? "/user" + : "https://hub.pena.digital/user"; export async function patchUser( user: PatchUserRequest ): Promise<[User | null, string?]> { try { const patchUserResponse = await makeRequest({ - url: apiUrl + "/user/", + url: apiUrl, contentType: true, method: "PATCH", useToken: true, diff --git a/src/api/verification.ts b/src/api/verification.ts index ee2c62d..2b90b00 100644 --- a/src/api/verification.ts +++ b/src/api/verification.ts @@ -6,14 +6,16 @@ import { parseAxiosError } from "@root/utils/parse-error"; import type { Verification, SendDocumentsArgs } from "@root/model/auth"; const apiUrl = - process.env.NODE_ENV === "production" ? "" : "https://hub.pena.digital"; + process.env.NODE_ENV === "production" + ? "/verification" + : "https://hub.pena.digital/verification"; export async function verification( userId: string ): Promise<[Verification | null, string?]> { try { const verificationResponse = await makeRequest({ - url: apiUrl + "/verification/verification/" + userId, + url: apiUrl + "/verification" + userId, method: "GET", useToken: true, withCredentials: true, @@ -32,7 +34,7 @@ export async function sendDocuments( ): Promise<[Verification | null, string?]> { try { const sendDocumentsResponse = await makeRequest({ - url: apiUrl + "/verification/verification", + url: apiUrl + "/verification", method: "POST", useToken: true, withCredentials: true, diff --git a/src/components/FloatingSupportChat/Chat.tsx b/src/components/FloatingSupportChat/Chat.tsx index a37b936..0cc3b53 100644 --- a/src/components/FloatingSupportChat/Chat.tsx +++ b/src/components/FloatingSupportChat/Chat.tsx @@ -1,265 +1,319 @@ -import { Box, FormControl, IconButton, InputAdornment, InputBase, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { + Box, + FormControl, + IconButton, + InputAdornment, + InputBase, + SxProps, + Theme, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; import { TicketMessage } from "@frontend/kitui"; -import { addOrUpdateUnauthMessages, useUnauthTicketStore, incrementUnauthMessageApiPage, setUnauthIsPreventAutoscroll, setUnauthSessionData, setIsMessageSending, setUnauthTicketMessageFetchState } from "@root/stores/unauthTicket"; +import { + addOrUpdateUnauthMessages, + useUnauthTicketStore, + incrementUnauthMessageApiPage, + setUnauthIsPreventAutoscroll, + setUnauthSessionData, + setIsMessageSending, + setUnauthTicketMessageFetchState, +} from "@root/stores/unauthTicket"; import { enqueueSnackbar } from "notistack"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import ChatMessage from "../ChatMessage"; import SendIcon from "../icons/SendIcon"; import UserCircleIcon from "./UserCircleIcon"; import { throttle } from "@frontend/kitui"; -import { makeRequest } from "@frontend/kitui"; -import { useTicketMessages, getMessageFromFetchError, useSSESubscription, useEventListener, createTicket } from "@frontend/kitui"; - +import { + useTicketMessages, + getMessageFromFetchError, + useSSESubscription, + useEventListener, + createTicket, +} from "@frontend/kitui"; +import { sendTicketMessage } from "@root/api/ticket"; interface Props { - sx?: SxProps; + sx?: SxProps; } export default function Chat({ sx }: Props) { - const theme = useTheme(); - const upMd = useMediaQuery(theme.breakpoints.up("md")); - const [messageField, setMessageField] = useState(""); - const sessionData = useUnauthTicketStore(state => state.sessionData); - const messages = useUnauthTicketStore(state => state.messages); - const messageApiPage = useUnauthTicketStore(state => state.apiPage); - const messagesPerPage = useUnauthTicketStore(state => state.messagesPerPage); - const isMessageSending = useUnauthTicketStore(state => state.isMessageSending); - const isPreventAutoscroll = useUnauthTicketStore(state => state.isPreventAutoscroll); - const lastMessageId = useUnauthTicketStore(state => state.lastMessageId); - const fetchState = useUnauthTicketStore(state => state.unauthTicketMessageFetchState); - const chatBoxRef = useRef(null); + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const [messageField, setMessageField] = useState(""); + const sessionData = useUnauthTicketStore((state) => state.sessionData); + const messages = useUnauthTicketStore((state) => state.messages); + const messageApiPage = useUnauthTicketStore((state) => state.apiPage); + const messagesPerPage = useUnauthTicketStore( + (state) => state.messagesPerPage + ); + const isMessageSending = useUnauthTicketStore( + (state) => state.isMessageSending + ); + const isPreventAutoscroll = useUnauthTicketStore( + (state) => state.isPreventAutoscroll + ); + const lastMessageId = useUnauthTicketStore((state) => state.lastMessageId); + const fetchState = useUnauthTicketStore( + (state) => state.unauthTicketMessageFetchState + ); + const chatBoxRef = useRef(null); - useTicketMessages({ - url: "https://admin.pena.digital/heruvym/getMessages", - isUnauth: true, - ticketId: sessionData?.ticketId, - messagesPerPage, - messageApiPage, - onSuccess: useCallback(messages => { - if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) chatBoxRef.current.scrollTop = 1; - addOrUpdateUnauthMessages(messages); - }, []), - onError: useCallback((error: Error) => { - const message = getMessageFromFetchError(error); - if (message) enqueueSnackbar(message); - }, []), - onFetchStateChange: setUnauthTicketMessageFetchState, - }); + useTicketMessages({ + url: "https://hub.pena.digital/heruvym/getMessages", + isUnauth: true, + ticketId: sessionData?.ticketId, + messagesPerPage, + messageApiPage, + onSuccess: useCallback((messages) => { + if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) + chatBoxRef.current.scrollTop = 1; + addOrUpdateUnauthMessages(messages); + }, []), + onError: useCallback((error: Error) => { + const message = getMessageFromFetchError(error); + if (message) enqueueSnackbar(message); + }, []), + onFetchStateChange: setUnauthTicketMessageFetchState, + }); - useSSESubscription({ - enabled: Boolean(sessionData), - url: `https://hub.pena.digital/heruvym/ticket?ticket=${sessionData?.ticketId}&s=${sessionData?.sessionId}`, - onNewData: addOrUpdateUnauthMessages, - onDisconnect: useCallback(() => { - setUnauthIsPreventAutoscroll(false); - }, []), - marker: "ticket" - }); + useSSESubscription({ + enabled: Boolean(sessionData), + url: `https://hub.pena.digital/heruvym/ticket?ticket=${sessionData?.ticketId}&s=${sessionData?.sessionId}`, + onNewData: addOrUpdateUnauthMessages, + onDisconnect: useCallback(() => { + setUnauthIsPreventAutoscroll(false); + }, []), + marker: "ticket", + }); - const throttledScrollHandler = useMemo(() => throttle(() => { + const throttledScrollHandler = useMemo( + () => + throttle(() => { const chatBox = chatBoxRef.current; if (!chatBox) return; - const scrollBottom = chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight; + const scrollBottom = + chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight; const isPreventAutoscroll = scrollBottom > chatBox.clientHeight; setUnauthIsPreventAutoscroll(isPreventAutoscroll); if (fetchState !== "idle") return; if (chatBox.scrollTop < chatBox.clientHeight) { - incrementUnauthMessageApiPage(); + incrementUnauthMessageApiPage(); } - }, 200), [fetchState]); + }, 200), + [fetchState] + ); - useEventListener("scroll", throttledScrollHandler, chatBoxRef); + useEventListener("scroll", throttledScrollHandler, chatBoxRef); - useEffect(function scrollOnNewMessage() { - if (!chatBoxRef.current) return; + useEffect( + function scrollOnNewMessage() { + if (!chatBoxRef.current) return; - if (!isPreventAutoscroll) { - setTimeout(() => { - scrollToBottom(); - }, 50); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [lastMessageId]); + if (!isPreventAutoscroll) { + setTimeout(() => { + scrollToBottom(); + }, 50); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, + [lastMessageId] + ); - async function handleSendMessage() { - if (!messageField || isMessageSending) return; + async function handleSendMessage() { + if (!messageField || isMessageSending) return; - if (!sessionData) { - setIsMessageSending(true); - createTicket({ - url: "https://hub.pena.digital/heruvym/create", - body: { - Title: "Unauth title", - Message: messageField, - }, - useToken: false, - }).then(response => { - setUnauthSessionData({ - ticketId: response.Ticket, - sessionId: response.sess, - }); - }).catch(error => { - const errorMessage = getMessageFromFetchError(error); - if (errorMessage) enqueueSnackbar(errorMessage); - }).finally(() => { - setMessageField(""); - setIsMessageSending(false); - }); - } else { - setIsMessageSending(true); - makeRequest({ - url: "https://hub.pena.digital/heruvym/send", - method: "POST", - useToken: false, - body: { - ticket: sessionData.ticketId, - message: messageField, - lang: "ru", - files: [], - }, - withCredentials: true, - }).catch(error => { - const errorMessage = getMessageFromFetchError(error); - if (errorMessage) enqueueSnackbar(errorMessage); - }).finally(() => { - setMessageField(""); - setIsMessageSending(false); - }); - } - - } - - function scrollToBottom(behavior?: ScrollBehavior) { - if (!chatBoxRef.current) return; - - const chatBox = chatBoxRef.current; - chatBox.scroll({ - left: 0, - top: chatBox.scrollHeight, - behavior, + if (!sessionData) { + setIsMessageSending(true); + createTicket({ + url: "https://hub.pena.digital/heruvym/create", + body: { + Title: "Unauth title", + Message: messageField, + }, + useToken: false, + }) + .then((response) => { + setUnauthSessionData({ + ticketId: response.Ticket, + sessionId: response.sess, + }); + }) + .catch((error) => { + const errorMessage = getMessageFromFetchError(error); + if (errorMessage) enqueueSnackbar(errorMessage); + }) + .finally(() => { + setMessageField(""); + setIsMessageSending(false); }); + } else { + setIsMessageSending(true); + + const [_, sendTicketMessageError] = await sendTicketMessage( + sessionData.ticketId, + messageField + ); + + if (sendTicketMessageError) { + enqueueSnackbar(sendTicketMessageError); + } + + setMessageField(""); + setIsMessageSending(false); } + } - const handleTextfieldKeyPress: React.KeyboardEventHandler = (e) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - handleSendMessage(); - } - }; + function scrollToBottom(behavior?: ScrollBehavior) { + if (!chatBoxRef.current) return; - return ( - = (e) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }; + + return ( + + + + - - - - Мария - онлайн-консультант - - - - - {sessionData && messages.map((message) => ( - - ))} - - - setMessageField(e.target.value)} - endAdornment={ - - - - - - } - /> - - + gap: "3px", + }} + > + Мария + + онлайн-консультант + - ); + + + + {sessionData && + messages.map((message) => ( + + ))} + + + setMessageField(e.target.value)} + endAdornment={ + + + + + + } + /> + + + + ); } diff --git a/src/components/ProtectedLayout.tsx b/src/components/ProtectedLayout.tsx index db10241..3bdb3c8 100644 --- a/src/components/ProtectedLayout.tsx +++ b/src/components/ProtectedLayout.tsx @@ -17,7 +17,7 @@ export default function ProtectedLayout() { const ticketsPerPage = useTicketStore((state) => state.ticketsPerPage); useSSESubscription({ - url: `https://admin.pena.digital/heruvym/subscribe?Authorization=${token}`, + url: `https://hub.pena.digital/heruvym/subscribe?Authorization=${token}`, onNewData: data => { updateTickets(data.filter(d => Boolean(d.id))); setTicketCount(data.length); @@ -49,14 +49,9 @@ export default function ProtectedLayout() { }); useCustomTariffs({ - url: "https://admin.pena.digital/strator/privilege/service", onNewUser: setCustomTariffs, onError: (error) => { - const errorMessage = getMessageFromFetchError( - error, - "Не удалось получить кастомные тарифы" - ); - if (errorMessage) enqueueSnackbar(errorMessage); + if (error) enqueueSnackbar(error); }, }); diff --git a/src/model/auth.ts b/src/model/auth.ts index a6073fa..5a463dc 100644 --- a/src/model/auth.ts +++ b/src/model/auth.ts @@ -1,6 +1,6 @@ import type { Attachment } from "@root/model/attachment"; -type File = { +export type File = { name: "inn" | "rule" | "egrule" | "certificate"; url: string; }; diff --git a/src/pages/Support/SupportChat.tsx b/src/pages/Support/SupportChat.tsx index b5c2ed5..d275a7b 100644 --- a/src/pages/Support/SupportChat.tsx +++ b/src/pages/Support/SupportChat.tsx @@ -1,329 +1,320 @@ import { - Box, - Button, - Fab, - FormControl, - IconButton, - InputAdornment, - InputBase, - Typography, - useMediaQuery, - useTheme, + Box, + Button, + Fab, + FormControl, + IconButton, + InputAdornment, + InputBase, + Typography, + useMediaQuery, + useTheme, } from "@mui/material"; import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useParams } from "react-router-dom"; import SendIcon from "@components/icons/SendIcon"; -import { makeRequest, throttle, useToken } from "@frontend/kitui"; +import { throttle, useToken } from "@frontend/kitui"; import { enqueueSnackbar } from "notistack"; import { useTicketStore } from "@root/stores/tickets"; import { - addOrUpdateMessages, - clearMessageState, - incrementMessageApiPage, - setIsPreventAutoscroll, - setTicketMessageFetchState, - useMessageStore, + addOrUpdateMessages, + clearMessageState, + incrementMessageApiPage, + setIsPreventAutoscroll, + setTicketMessageFetchState, + useMessageStore, } from "@root/stores/messages"; import { TicketMessage } from "@frontend/kitui"; import ChatMessage from "@root/components/ChatMessage"; import { cardShadow } from "@root/utils/theme"; import { - getMessageFromFetchError, - useEventListener, - useSSESubscription, - useTicketMessages, + getMessageFromFetchError, + useEventListener, + useSSESubscription, + useTicketMessages, } from "@frontend/kitui"; +import { shownMessage, sendTicketMessage } from "@root/api/ticket"; export default function SupportChat() { - const theme = useTheme(); - const upMd = useMediaQuery(theme.breakpoints.up("md")); - const isMobile = useMediaQuery(theme.breakpoints.up(460)); - const [messageField, setMessageField] = useState(""); - const tickets = useTicketStore((state) => state.tickets); - const messages = useMessageStore((state) => state.messages); - const messageApiPage = useMessageStore((state) => state.apiPage); - const lastMessageId = useMessageStore((state) => state.lastMessageId); - const messagesPerPage = useMessageStore((state) => state.messagesPerPage); - const isPreventAutoscroll = useMessageStore( - (state) => state.isPreventAutoscroll - ); - const token = useToken(); - const ticketId = useParams().ticketId; - const ticket = tickets.find((ticket) => ticket.id === ticketId); - const chatBoxRef = useRef(null); - const fetchState = useMessageStore(state => state.ticketMessageFetchState); + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const isMobile = useMediaQuery(theme.breakpoints.up(460)); + const [messageField, setMessageField] = useState(""); + const tickets = useTicketStore((state) => state.tickets); + const messages = useMessageStore((state) => state.messages); + const messageApiPage = useMessageStore((state) => state.apiPage); + const lastMessageId = useMessageStore((state) => state.lastMessageId); + const messagesPerPage = useMessageStore((state) => state.messagesPerPage); + const isPreventAutoscroll = useMessageStore( + (state) => state.isPreventAutoscroll + ); + const token = useToken(); + const ticketId = useParams().ticketId; + const ticket = tickets.find((ticket) => ticket.id === ticketId); + const chatBoxRef = useRef(null); + const fetchState = useMessageStore((state) => state.ticketMessageFetchState); - useTicketMessages({ - url: "https://admin.pena.digital/heruvym/getMessages", - ticketId, - messagesPerPage, - messageApiPage, - onSuccess: (messages) => { - if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) - chatBoxRef.current.scrollTop = 1; - addOrUpdateMessages(messages); - }, - onError: (error: Error) => { - const message = getMessageFromFetchError(error); - if (message) enqueueSnackbar(message); - }, - onFetchStateChange: setTicketMessageFetchState, - }); + useTicketMessages({ + url: "https://hub.pena.digital/heruvym/getMessages", + ticketId, + messagesPerPage, + messageApiPage, + onSuccess: (messages) => { + if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) + chatBoxRef.current.scrollTop = 1; + addOrUpdateMessages(messages); + }, + onError: (error: Error) => { + const message = getMessageFromFetchError(error); + if (message) enqueueSnackbar(message); + }, + onFetchStateChange: setTicketMessageFetchState, + }); - useSSESubscription({ - enabled: Boolean(token) && Boolean(ticketId), - url: `https://admin.pena.digital/heruvym/ticket?ticket=${ticketId}&Authorization=${token}`, - onNewData: addOrUpdateMessages, - onDisconnect: useCallback(() => { - clearMessageState(); - setIsPreventAutoscroll(false); - }, []), - marker: "ticket message", - }); - - const throttledScrollHandler = useMemo( - () => - throttle(() => { - const chatBox = chatBoxRef.current; - if (!chatBox) return; - - const scrollBottom = - chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight; - const isPreventAutoscroll = scrollBottom > chatBox.clientHeight * 20; - setIsPreventAutoscroll(isPreventAutoscroll); - - if (fetchState !== "idle") return; - - if (chatBox.scrollTop < chatBox.clientHeight) { - incrementMessageApiPage(); - } - }, 200), - [fetchState] - ); - - useEventListener("scroll", throttledScrollHandler, chatBoxRef); - - useEffect( - function scrollOnNewMessage() { - if (!chatBoxRef.current) return; - - if (!isPreventAutoscroll) { - setTimeout(() => { - scrollToBottom(); - }, 50); - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [lastMessageId] - ); - - useEffect(() => { - if (ticket) - makeRequest({ - url: "https://admin.pena.digital/heruvym/shown", - method: "POST", - useToken: true, - body: { id: ticket.top_message.id }, - }); - }, [ticket]); - - function handleSendMessage() { - if (!ticket || !messageField) return; - - makeRequest({ - url: "https://hub.pena.digital/heruvym/send", - method: "POST", - useToken: true, - body: { - ticket: ticket.id, - message: messageField, - lang: "ru", - files: [], - }, - }) - .then(() => { - setMessageField(""); - }) - .catch((error) => { - const errorMessage = getMessageFromFetchError(error); - if (errorMessage) enqueueSnackbar(errorMessage); - }); - } - - function scrollToBottom(behavior?: ScrollBehavior) { - if (!chatBoxRef.current) return; + useSSESubscription({ + enabled: Boolean(token) && Boolean(ticketId), + url: `https://hub.pena.digital/heruvym/ticket?ticket=${ticketId}&Authorization=${token}`, + onNewData: addOrUpdateMessages, + onDisconnect: useCallback(() => { + clearMessageState(); + setIsPreventAutoscroll(false); + }, []), + marker: "ticket message", + }); + const throttledScrollHandler = useMemo( + () => + throttle(() => { const chatBox = chatBoxRef.current; - chatBox.scroll({ - left: 0, - top: chatBox.scrollHeight, - behavior, - }); + if (!chatBox) return; + + const scrollBottom = + chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight; + const isPreventAutoscroll = scrollBottom > chatBox.clientHeight * 20; + setIsPreventAutoscroll(isPreventAutoscroll); + + if (fetchState !== "idle") return; + + if (chatBox.scrollTop < chatBox.clientHeight) { + incrementMessageApiPage(); + } + }, 200), + [fetchState] + ); + + useEventListener("scroll", throttledScrollHandler, chatBoxRef); + + useEffect( + function scrollOnNewMessage() { + if (!chatBoxRef.current) return; + + if (!isPreventAutoscroll) { + setTimeout(() => { + scrollToBottom(); + }, 50); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [lastMessageId] + ); + + useEffect(() => { + if (ticket) { + shownMessage(ticket.top_message.id); + } + }, [ticket]); + + async function handleSendMessage() { + if (!ticket || !messageField) return; + + const [_, sendTicketMessageError] = await sendTicketMessage( + ticket.id, + messageField + ); + + if (sendTicketMessageError) { + return enqueueSnackbar(sendTicketMessageError); } - const createdAt = ticket && new Date(ticket.created_at); - const createdAtString = - createdAt && - createdAt.toLocaleDateString(undefined, { - year: "numeric", - month: "2-digit", - day: "2-digit", - }) + - " " + - createdAt.toLocaleTimeString(undefined, { - hour: "2-digit", - minute: "2-digit", - }); + setMessageField(""); + } - return ( - + + + {ticket?.title} + + - + + + {isPreventAutoscroll && ( + scrollToBottom("smooth")} sx={{ - display: "flex", - alignItems: "start", - flexDirection: "column", - flexGrow: 1, + position: "absolute", + left: "10px", + bottom: "10px", }} - > - - {ticket?.title} - - - Создан: {createdAtString} - - - - {isPreventAutoscroll && ( - scrollToBottom("smooth")} - sx={{ - position: "absolute", - left: "10px", - bottom: "10px", - }} - > - - - )} - - {ticket && - messages.map((message) => ( - - ))} - - - - setMessageField(e.target.value)} - endAdornment={ - !upMd && ( - - - - - - ) - } - /> - - - - {upMd && ( - - - + > + + )} + + {ticket && + messages.map((message) => ( + + ))} + + + + setMessageField(e.target.value)} + endAdornment={ + !upMd && ( + + + + + + ) + } + /> + - ); + + {upMd && ( + + + + )} + + ); } diff --git a/src/pages/auth/Signin.tsx b/src/pages/auth/Signin.tsx index c11a378..70d7ad1 100644 --- a/src/pages/auth/Signin.tsx +++ b/src/pages/auth/Signin.tsx @@ -6,7 +6,7 @@ import { Typography, useMediaQuery, useTheme, - Button + Button, } from "@mui/material"; import CloseIcon from "@mui/icons-material/Close"; import { useLocation, useNavigate } from "react-router-dom"; @@ -18,20 +18,19 @@ import { Link as RouterLink } from "react-router-dom"; import { object, string } from "yup"; import { useEffect, useState } from "react"; import { setUserId, useUserStore } from "@root/stores/user"; -import { LoginRequest, LoginResponse, getMessageFromFetchError } from "@frontend/kitui"; -import { makeRequest } from "@frontend/kitui"; import { cardShadow } from "@root/utils/theme"; import PasswordInput from "@root/components/passwordInput"; import AmoButton from "./AmoButton"; +import { login } from "@root/api/auth"; interface Values { - email: string; - password: string; + email: string; + password: string; } const initialValues: Values = { - email: "", - password: "", + email: "", + password: "", }; const validationSchema = object({ @@ -42,153 +41,150 @@ const validationSchema = object({ }); export default function SigninDialog() { - const [isDialogOpen, setIsDialogOpen] = useState(true); - const user = useUserStore((state) => state.user); - const theme = useTheme(); - const upMd = useMediaQuery(theme.breakpoints.up("md")); - const navigate = useNavigate(); - const location = useLocation(); - const formik = useFormik({ - initialValues, - validationSchema, - onSubmit: (values, formikHelpers) => { - makeRequest({ - url: "https://hub.pena.digital/auth/login", - body: { - login: values.email.trim(), - password: values.password.trim(), - }, - useToken: false, - withCredentials: true, - }) - .then((result) => { - setUserId(result._id); - }) - .catch((error: Error) => { - const errorMessage = getMessageFromFetchError(error); - if (errorMessage) enqueueSnackbar(errorMessage); - }) - .finally(() => { - formikHelpers.setSubmitting(false); - }); + const [isDialogOpen, setIsDialogOpen] = useState(true); + const user = useUserStore((state) => state.user); + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const navigate = useNavigate(); + const location = useLocation(); + const formik = useFormik({ + initialValues, + validationSchema, + onSubmit: async (values, formikHelpers) => { + const [loginResponse, loginError] = await login( + values.email.trim(), + values.password.trim() + ); + + formikHelpers.setSubmitting(false); + + if (loginError) { + return enqueueSnackbar(loginError); + } + + if (loginResponse) { + setUserId(loginResponse._id); + } + }, + }); + + useEffect( + function redirectIfSignedIn() { + if (user) navigate("/tariffs", { replace: true }); + }, + [navigate, user] + ); + + function handleClose() { + setIsDialogOpen(false); + setTimeout(() => navigate("/"), theme.transitions.duration.leavingScreen); + } + + return ( + navigate("/"), theme.transitions.duration.leavingScreen); - } - - return ( - + + - - - - - - - - - Вход в личный кабинет - - - - - {/* + + + + + + Вход в личный кабинет + + + + + {/* Забыли пароль? */} - - - Вы еще не присоединились? - - - Регистрация - - - - - - ); + + + Вы еще не присоединились? + + + Регистрация + + + + + + ); } diff --git a/src/pages/auth/Signup.tsx b/src/pages/auth/Signup.tsx index 77199e4..f9e9b91 100644 --- a/src/pages/auth/Signup.tsx +++ b/src/pages/auth/Signup.tsx @@ -1,4 +1,13 @@ -import { Box, Button, Dialog, IconButton, Link, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { + Box, + Button, + Dialog, + IconButton, + Link, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; import { useLocation, useNavigate } from "react-router-dom"; import { useFormik } from "formik"; import CloseIcon from "@mui/icons-material/Close"; @@ -9,210 +18,211 @@ import { Link as RouterLink } from "react-router-dom"; import { object, ref, string } from "yup"; import { useEffect, useState } from "react"; import { setUserId, useUserStore } from "@root/stores/user"; -import { RegisterRequest, RegisterResponse, getMessageFromFetchError } from "@frontend/kitui"; -import { makeRequest } from "@frontend/kitui"; import { cardShadow } from "@root/utils/theme"; import PasswordInput from "@root/components/passwordInput"; import AmoButton from "./AmoButton"; +import { register } from "@root/api/auth"; interface Values { - email: string; - password: string; - repeatPassword: string; + email: string; + password: string; + repeatPassword: string; } const initialValues: Values = { - email: "", - password: "", - repeatPassword: "", + email: "", + password: "", + repeatPassword: "", }; const validationSchema = object({ - email: string().required("Поле обязательно").email("Введите корректный email"), - password: string() - .min(8, "Минимум 8 символов") - .matches(/^[.,:;-_+\d\w]+$/, "Некорректные символы") - .required("Поле обязательно"), - repeatPassword: string() - .oneOf([ref("password"), undefined], "Пароли не совпадают") - .required("Повторите пароль"), + email: string() + .required("Поле обязательно") + .email("Введите корректный email"), + password: string() + .min(8, "Минимум 8 символов") + .matches(/^[.,:;-_+\d\w]+$/, "Некорректные символы") + .required("Поле обязательно"), + repeatPassword: string() + .oneOf([ref("password"), undefined], "Пароли не совпадают") + .required("Повторите пароль"), }); export default function SignupDialog() { - const [isDialogOpen, setIsDialogOpen] = useState(true); - const user = useUserStore((state) => state.user); - const theme = useTheme(); - const upMd = useMediaQuery(theme.breakpoints.up("md")); - const navigate = useNavigate(); - const location = useLocation(); - const formik = useFormik({ - initialValues, - validationSchema, - onSubmit: (values, formikHelpers) => { - makeRequest({ - url: "https://hub.pena.digital/auth/register", - body: { - login: values.email.trim(), - password: values.password.trim(), - phoneNumber: "+7", - }, - useToken: false, - withCredentials: true, - }) - .then((result) => { - setUserId(result._id); - }) - .catch((error: any) => { - const errorMessage = getMessageFromFetchError(error); - if (errorMessage) enqueueSnackbar(errorMessage); - }) - .finally(() => { - formikHelpers.setSubmitting(false); - }); + const [isDialogOpen, setIsDialogOpen] = useState(true); + const user = useUserStore((state) => state.user); + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const navigate = useNavigate(); + const location = useLocation(); + const formik = useFormik({ + initialValues, + validationSchema, + onSubmit: async (values, formikHelpers) => { + const [registerResponse, registerError] = await register( + values.email.trim(), + values.password.trim(), + "+7" + ); + + formikHelpers.setSubmitting(false); + + if (registerError) { + return enqueueSnackbar(registerError); + } + + if (registerResponse) { + setUserId(registerResponse._id); + } + }, + }); + + useEffect( + function redirectIfSignedIn() { + if (user) navigate("/tariffs", { replace: true }); + }, + [navigate, user] + ); + + function handleClose() { + setIsDialogOpen(false); + setTimeout(() => navigate("/"), theme.transitions.duration.leavingScreen); + } + + return ( + navigate("/"), theme.transitions.duration.leavingScreen); - } - - return ( - + + - - - - - - - - - Регистрация - - - - - - - Вход в личный кабинет - - - - ); + + + + + + + Регистрация + + + + + + + Вход в личный кабинет + + + + ); } diff --git a/src/utils/hooks/useCustomTariffs.ts b/src/utils/hooks/useCustomTariffs.ts index 084740e..f5aa514 100644 --- a/src/utils/hooks/useCustomTariffs.ts +++ b/src/utils/hooks/useCustomTariffs.ts @@ -1,36 +1,38 @@ -import { devlog, makeRequest } from "@frontend/kitui"; -import { ServiceKeyToPrivilegesMap } from "@root/model/privilege"; import { useEffect, useLayoutEffect, useRef } from "react"; +import { devlog } from "@frontend/kitui"; +import { ServiceKeyToPrivilegesMap } from "@root/model/privilege"; +import { getCustomTariffs } from "@root/api/tariff"; -export function useCustomTariffs({ onError, onNewUser, url }: { - url: string; - onNewUser: (response: ServiceKeyToPrivilegesMap) => void; - onError: (error: any) => void; +export function useCustomTariffs({ + onError, + onNewUser, +}: { + onNewUser: (response: ServiceKeyToPrivilegesMap) => void; + onError: (error: any) => void; }) { - const onNewUserRef = useRef(onNewUser); - const onErrorRef = useRef(onError); + const onNewUserRef = useRef(onNewUser); + const onErrorRef = useRef(onError); - useLayoutEffect(() => { - onNewUserRef.current = onNewUser; - onErrorRef.current = onError; - }); + useLayoutEffect(() => { + onNewUserRef.current = onNewUser; + onErrorRef.current = onError; + }); - useEffect(() => { - const controller = new AbortController(); + useEffect(() => { + const controller = new AbortController(); - makeRequest({ - url, - signal: controller.signal, - method: "get", - useToken: true, - }).then(result => { - onNewUserRef.current(result); - }).catch(error => { - devlog("Error fetching custom tariffs", error); - onErrorRef.current(error); - }); + getCustomTariffs(controller.signal) + .then(([customTariffs]) => { + if (customTariffs) { + onNewUserRef.current(customTariffs); + } + }) + .catch(([_, error]) => { + devlog("Error fetching custom tariffs", error); + onErrorRef.current(error); + }); - return () => controller.abort(); - }, [url]); + return () => controller.abort(); + }, []); } diff --git a/src/utils/hooks/useDiscounts.ts b/src/utils/hooks/useDiscounts.ts index e73bbf6..7ff3fba 100644 --- a/src/utils/hooks/useDiscounts.ts +++ b/src/utils/hooks/useDiscounts.ts @@ -1,36 +1,38 @@ -import { Discount, devlog, makeRequest } from "@frontend/kitui"; -import { GetDiscountsResponse } from "@root/model/discount"; import { useEffect, useLayoutEffect, useRef } from "react"; +import { Discount, devlog } from "@frontend/kitui"; +import { getDiscounts } from "@root/api/price"; -export function useDiscounts({ url = "https://admin.pena.digital/price/discounts", onNewDiscounts, onError }: { - url?: string; - onNewDiscounts: (response: Discount[]) => void; - onError: (error: Error) => void; +export function useDiscounts({ + onNewDiscounts, + onError, +}: { + url?: string; + onNewDiscounts: (response: Discount[]) => void; + onError: (error: Error) => void; }) { - const onNewTariffsRef = useRef(onNewDiscounts); - const onErrorRef = useRef(onError); + const onNewTariffsRef = useRef(onNewDiscounts); + const onErrorRef = useRef(onError); - useLayoutEffect(() => { - onNewTariffsRef.current = onNewDiscounts; - onErrorRef.current = onError; - }, [onError, onNewDiscounts]); + useLayoutEffect(() => { + onNewTariffsRef.current = onNewDiscounts; + onErrorRef.current = onError; + }, [onError, onNewDiscounts]); - useEffect(() => { - const controller = new AbortController(); + useEffect(() => { + const controller = new AbortController(); - makeRequest({ - url, - method: "get", - useToken: true, - signal: controller.signal, - }).then((result) => { - onNewTariffsRef.current(result.Discounts); - }).catch(error => { - devlog("Error fetching tariffs", error); - onErrorRef.current(error); - }); + getDiscounts(controller.signal) + .then(([discounts]) => { + if (discounts) { + onNewTariffsRef.current(discounts.Discounts); + } + }) + .catch((error) => { + devlog("Error fetching tariffs", error); + onErrorRef.current(error); + }); - return () => controller.abort(); - }, [url]); + return () => controller.abort(); + }, []); } diff --git a/src/utils/hooks/useTariffFetcher.ts b/src/utils/hooks/useTariffFetcher.ts index fb4710c..2cc7929 100644 --- a/src/utils/hooks/useTariffFetcher.ts +++ b/src/utils/hooks/useTariffFetcher.ts @@ -1,50 +1,49 @@ -import { Tariff, makeRequest } from "@frontend/kitui"; -import { GetTariffsResponse } from "@root/model/tariff"; import { useEffect, useLayoutEffect, useRef, useState } from "react"; +import { getTariffs } from "@root/api/tariff"; + +import type { Tariff } from "@frontend/kitui"; export function useTariffFetcher({ - baseUrl = process.env.NODE_ENV === "production" ? "/strator/tariff" : "https://hub.pena.digital/strator/tariff", - tariffsPerPage, - apiPage, - onSuccess, - onError, + tariffsPerPage, + apiPage, + onSuccess, + onError, }: { - baseUrl?: string; - tariffsPerPage: number; - apiPage: number; - onSuccess: (response: Tariff[]) => void; - onError?: (error: Error) => void; + baseUrl?: string; + tariffsPerPage: number; + apiPage: number; + onSuccess: (response: Tariff[]) => void; + onError?: (error: Error) => void; }) { - const [fetchState, setFetchState] = useState<"fetching" | "idle" | "all fetched">("idle"); - const onSuccessRef = useRef(onSuccess); - const onErrorRef = useRef(onError); + const [fetchState, setFetchState] = useState< + "fetching" | "idle" | "all fetched" + >("idle"); + const onSuccessRef = useRef(onSuccess); + const onErrorRef = useRef(onError); - useLayoutEffect(() => { - onSuccessRef.current = onSuccess; - onErrorRef.current = onError; - }, [onError, onSuccess]); + useLayoutEffect(() => { + onSuccessRef.current = onSuccess; + onErrorRef.current = onError; + }, [onError, onSuccess]); - useEffect(() => { - const controller = new AbortController(); + useEffect(() => { + const controller = new AbortController(); - setFetchState("fetching"); - makeRequest({ - url: baseUrl + `?page=${apiPage}&limit=${tariffsPerPage}`, - method: "get", - useToken: true, - signal: controller.signal, - }).then((result) => { - if (result.tariffs.length > 0) { - onSuccessRef.current(result.tariffs); - setFetchState("idle"); - } else setFetchState("all fetched"); - }).catch(error => { - onErrorRef.current?.(error); - }); + setFetchState("fetching"); + getTariffs(apiPage, tariffsPerPage, controller.signal) + .then(([result]) => { + if (result && result.tariffs.length > 0) { + onSuccessRef.current(result.tariffs); + setFetchState("idle"); + } else setFetchState("all fetched"); + }) + .catch(([_, error]) => { + onErrorRef.current?.(error); + }); - return () => controller.abort(); - }, [apiPage, tariffsPerPage, baseUrl]); + return () => controller.abort(); + }, [apiPage, tariffsPerPage]); - return fetchState; + return fetchState; }