refactor: server requests
This commit is contained in:
parent
8d150fe34b
commit
801df79eba
@ -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<LoginRequest, LoginResponse>({
|
||||
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<never, void>({
|
||||
url: apiUrl + "/auth/logout",
|
||||
url: apiUrl + "/logout",
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
withCredentials: true,
|
||||
|
29
src/api/price.ts
Normal file
29
src/api/price.ts
Normal file
@ -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<never, GetDiscountsResponse>({
|
||||
url: apiUrl + "/discounts",
|
||||
method: "get",
|
||||
useToken: true,
|
||||
signal,
|
||||
});
|
||||
|
||||
return [discountsResponse];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
return [null, `Ошибка получения списка скидок. ${error}`];
|
||||
}
|
||||
}
|
@ -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<never, GetTariffsResponse>({
|
||||
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}`];
|
||||
}
|
||||
}
|
||||
|
50
src/api/ticket.ts
Normal file
50
src/api/ticket.ts
Normal file
@ -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}`];
|
||||
}
|
||||
}
|
@ -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<PatchUserRequest, User>({
|
||||
url: apiUrl + "/user/",
|
||||
url: apiUrl,
|
||||
contentType: true,
|
||||
method: "PATCH",
|
||||
useToken: true,
|
||||
|
@ -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<never, Verification>({
|
||||
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<FormData, Verification>({
|
||||
url: apiUrl + "/verification/verification",
|
||||
url: apiUrl + "/verification",
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
withCredentials: true,
|
||||
|
@ -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<Theme>;
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
|
||||
export default function Chat({ sx }: Props) {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const [messageField, setMessageField] = useState<string>("");
|
||||
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<HTMLDivElement>(null);
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const [messageField, setMessageField] = useState<string>("");
|
||||
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<HTMLDivElement>(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<TicketMessage>({
|
||||
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<TicketMessage>({
|
||||
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<HTMLInputElement | HTMLTextAreaElement> = (e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
};
|
||||
function scrollToBottom(behavior?: ScrollBehavior) {
|
||||
if (!chatBoxRef.current) return;
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
const chatBox = chatBoxRef.current;
|
||||
chatBox.scroll({
|
||||
left: 0,
|
||||
top: chatBox.scrollHeight,
|
||||
behavior,
|
||||
});
|
||||
}
|
||||
|
||||
const handleTextfieldKeyPress: React.KeyboardEventHandler<
|
||||
HTMLInputElement | HTMLTextAreaElement
|
||||
> = (e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "clamp(250px, calc(100vh - 90px), 600px)",
|
||||
backgroundColor: "#944FEE",
|
||||
borderRadius: "8px",
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "9px",
|
||||
pl: "22px",
|
||||
pt: "12px",
|
||||
pb: "20px",
|
||||
filter: "drop-shadow(0px 3px 12px rgba(37, 39, 52, 0.3))",
|
||||
}}
|
||||
>
|
||||
<UserCircleIcon />
|
||||
<Box
|
||||
sx={{
|
||||
mt: "5px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "clamp(250px, calc(100vh - 90px), 600px)",
|
||||
backgroundColor: "#944FEE",
|
||||
borderRadius: "8px",
|
||||
...sx,
|
||||
}}>
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
gap: "9px",
|
||||
pl: "22px",
|
||||
pt: "12px",
|
||||
pb: "20px",
|
||||
filter: "drop-shadow(0px 3px 12px rgba(37, 39, 52, 0.3))",
|
||||
}}>
|
||||
<UserCircleIcon />
|
||||
<Box sx={{
|
||||
mt: "5px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "3px",
|
||||
}}>
|
||||
<Typography>Мария</Typography>
|
||||
<Typography sx={{
|
||||
fontSize: "16px",
|
||||
lineHeight: "19px",
|
||||
}}>онлайн-консультант</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{
|
||||
flexGrow: 1,
|
||||
backgroundColor: "white",
|
||||
borderRadius: "8px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}>
|
||||
<Box
|
||||
ref={chatBoxRef}
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
flexBasis: 0,
|
||||
flexDirection: "column",
|
||||
gap: upMd ? "20px" : "16px",
|
||||
px: upMd ? "20px" : "5px",
|
||||
py: upMd ? "20px" : "13px",
|
||||
overflowY: "auto",
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
{sessionData && messages.map((message) => (
|
||||
<ChatMessage
|
||||
unAuthenticated
|
||||
key={message.id}
|
||||
text={message.message}
|
||||
createdAt={message.created_at}
|
||||
isSelf={sessionData.sessionId === message.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={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>
|
||||
</Box>
|
||||
gap: "3px",
|
||||
}}
|
||||
>
|
||||
<Typography>Мария</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
lineHeight: "19px",
|
||||
}}
|
||||
>
|
||||
онлайн-консультант
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
backgroundColor: "white",
|
||||
borderRadius: "8px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
ref={chatBoxRef}
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
flexBasis: 0,
|
||||
flexDirection: "column",
|
||||
gap: upMd ? "20px" : "16px",
|
||||
px: upMd ? "20px" : "5px",
|
||||
py: upMd ? "20px" : "13px",
|
||||
overflowY: "auto",
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
{sessionData &&
|
||||
messages.map((message) => (
|
||||
<ChatMessage
|
||||
unAuthenticated
|
||||
key={message.id}
|
||||
text={message.message}
|
||||
createdAt={message.created_at}
|
||||
isSelf={sessionData.sessionId === message.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={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>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ export default function ProtectedLayout() {
|
||||
const ticketsPerPage = useTicketStore((state) => state.ticketsPerPage);
|
||||
|
||||
useSSESubscription<Ticket>({
|
||||
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);
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { Attachment } from "@root/model/attachment";
|
||||
|
||||
type File = {
|
||||
export type File = {
|
||||
name: "inn" | "rule" | "egrule" | "certificate";
|
||||
url: string;
|
||||
};
|
||||
|
@ -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<string>("");
|
||||
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<HTMLDivElement>(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<string>("");
|
||||
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<HTMLDivElement>(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<TicketMessage>({
|
||||
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<TicketMessage>({
|
||||
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 (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: upMd ? "white" : undefined,
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
maxHeight: upMd ? "443px" : undefined,
|
||||
borderRadius: "12px",
|
||||
p: upMd ? "20px" : undefined,
|
||||
gap: "40px",
|
||||
height: !upMd ? `calc(100% - ${isMobile ? 90 : 115}px)` : null,
|
||||
boxShadow: upMd ? cardShadow : undefined,
|
||||
}}
|
||||
function scrollToBottom(behavior?: ScrollBehavior) {
|
||||
if (!chatBoxRef.current) return;
|
||||
|
||||
const chatBox = chatBoxRef.current;
|
||||
chatBox.scroll({
|
||||
left: 0,
|
||||
top: chatBox.scrollHeight,
|
||||
behavior,
|
||||
});
|
||||
}
|
||||
|
||||
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",
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: upMd ? "white" : undefined,
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
maxHeight: upMd ? "443px" : undefined,
|
||||
borderRadius: "12px",
|
||||
p: upMd ? "20px" : undefined,
|
||||
gap: "40px",
|
||||
height: !upMd ? `calc(100% - ${isMobile ? 90 : 115}px)` : null,
|
||||
boxShadow: upMd ? cardShadow : undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "start",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
<Typography variant={upMd ? "h5" : "body2"} mb={"4px"}>
|
||||
{ticket?.title}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 400,
|
||||
fontSize: "14px",
|
||||
lineHeight: "17px",
|
||||
color: theme.palette.gray.main,
|
||||
mb: upMd ? "9px" : "20px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
Создан: {createdAtString}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: "#ECECF3",
|
||||
border: `1px solid ${theme.palette.gray.main}`,
|
||||
borderRadius: "10px",
|
||||
overflow: "hidden",
|
||||
width: "100%",
|
||||
minHeight: "345px",
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
flexGrow: 1,
|
||||
borderBottom: `1px solid ${theme.palette.gray.main}`,
|
||||
height: "200px",
|
||||
}}
|
||||
>
|
||||
{isPreventAutoscroll && (
|
||||
<Fab
|
||||
size="small"
|
||||
onClick={() => scrollToBottom("smooth")}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "start",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
position: "absolute",
|
||||
left: "10px",
|
||||
bottom: "10px",
|
||||
}}
|
||||
>
|
||||
<Typography variant={upMd ? "h5" : "body2"} mb={"4px"}>
|
||||
{ticket?.title}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 400,
|
||||
fontSize: "14px",
|
||||
lineHeight: "17px",
|
||||
color: theme.palette.gray.main,
|
||||
mb: upMd ? "9px" : "20px",
|
||||
}}
|
||||
>
|
||||
Создан: {createdAtString}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: "#ECECF3",
|
||||
border: `1px solid ${theme.palette.gray.main}`,
|
||||
borderRadius: "10px",
|
||||
overflow: "hidden",
|
||||
width: "100%",
|
||||
minHeight: "345px",
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
flexGrow: 1,
|
||||
borderBottom: `1px solid ${theme.palette.gray.main}`,
|
||||
height: "200px",
|
||||
}}
|
||||
>
|
||||
{isPreventAutoscroll && (
|
||||
<Fab
|
||||
size="small"
|
||||
onClick={() => scrollToBottom("smooth")}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
left: "10px",
|
||||
bottom: "10px",
|
||||
}}
|
||||
>
|
||||
<ArrowDownwardIcon />
|
||||
</Fab>
|
||||
)}
|
||||
<Box
|
||||
ref={chatBoxRef}
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
flexDirection: "column",
|
||||
gap: upMd ? "20px" : "16px",
|
||||
px: upMd ? "20px" : "5px",
|
||||
py: upMd ? "20px" : "13px",
|
||||
overflowY: "auto",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
{ticket &&
|
||||
messages.map((message) => (
|
||||
<ChatMessage
|
||||
key={message.id}
|
||||
text={message.message}
|
||||
createdAt={message.created_at}
|
||||
isSelf={ticket.user === message.user_id}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
<FormControl>
|
||||
<InputBase
|
||||
value={messageField}
|
||||
fullWidth
|
||||
placeholder="Текст обращения"
|
||||
id="message"
|
||||
multiline
|
||||
sx={{
|
||||
width: "100%",
|
||||
p: 0,
|
||||
}}
|
||||
inputProps={{
|
||||
sx: {
|
||||
fontWeight: 400,
|
||||
fontSize: "16px",
|
||||
lineHeight: "19px",
|
||||
pt: upMd ? "13px" : "28px",
|
||||
pb: upMd ? "13px" : "24px",
|
||||
px: "19px",
|
||||
maxHeight: "calc(19px * 5)",
|
||||
},
|
||||
}}
|
||||
onChange={(e) => setMessageField(e.target.value)}
|
||||
endAdornment={
|
||||
!upMd && (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={handleSendMessage}
|
||||
sx={{
|
||||
height: "45px",
|
||||
width: "45px",
|
||||
mr: "13px",
|
||||
p: 0,
|
||||
}}
|
||||
>
|
||||
<SendIcon />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</Box>
|
||||
{upMd && (
|
||||
<Box sx={{ alignSelf: "end" }}>
|
||||
<Button
|
||||
variant="pena-contained-dark"
|
||||
onClick={handleSendMessage}
|
||||
disabled={!messageField}
|
||||
>Отправить</Button>
|
||||
</Box>
|
||||
>
|
||||
<ArrowDownwardIcon />
|
||||
</Fab>
|
||||
)}
|
||||
<Box
|
||||
ref={chatBoxRef}
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
flexDirection: "column",
|
||||
gap: upMd ? "20px" : "16px",
|
||||
px: upMd ? "20px" : "5px",
|
||||
py: upMd ? "20px" : "13px",
|
||||
overflowY: "auto",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
{ticket &&
|
||||
messages.map((message) => (
|
||||
<ChatMessage
|
||||
key={message.id}
|
||||
text={message.message}
|
||||
createdAt={message.created_at}
|
||||
isSelf={ticket.user === message.user_id}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
<FormControl>
|
||||
<InputBase
|
||||
value={messageField}
|
||||
fullWidth
|
||||
placeholder="Текст обращения"
|
||||
id="message"
|
||||
multiline
|
||||
sx={{
|
||||
width: "100%",
|
||||
p: 0,
|
||||
}}
|
||||
inputProps={{
|
||||
sx: {
|
||||
fontWeight: 400,
|
||||
fontSize: "16px",
|
||||
lineHeight: "19px",
|
||||
pt: upMd ? "13px" : "28px",
|
||||
pb: upMd ? "13px" : "24px",
|
||||
px: "19px",
|
||||
maxHeight: "calc(19px * 5)",
|
||||
},
|
||||
}}
|
||||
onChange={(e) => setMessageField(e.target.value)}
|
||||
endAdornment={
|
||||
!upMd && (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={handleSendMessage}
|
||||
sx={{
|
||||
height: "45px",
|
||||
width: "45px",
|
||||
mr: "13px",
|
||||
p: 0,
|
||||
}}
|
||||
>
|
||||
<SendIcon />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</Box>
|
||||
);
|
||||
</Box>
|
||||
{upMd && (
|
||||
<Box sx={{ alignSelf: "end" }}>
|
||||
<Button
|
||||
variant="pena-contained-dark"
|
||||
onClick={handleSendMessage}
|
||||
disabled={!messageField}
|
||||
>
|
||||
Отправить
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -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<boolean>(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<Values>({
|
||||
initialValues,
|
||||
validationSchema,
|
||||
onSubmit: (values, formikHelpers) => {
|
||||
makeRequest<LoginRequest, LoginResponse>({
|
||||
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<boolean>(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<Values>({
|
||||
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 (
|
||||
<Dialog
|
||||
open={isDialogOpen}
|
||||
onClose={handleClose}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
width: "600px",
|
||||
maxWidth: "600px",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(
|
||||
function redirectIfSignedIn() {
|
||||
if (user) navigate("/tariffs", { replace: true });
|
||||
}}
|
||||
slotProps={{
|
||||
backdrop: {
|
||||
style: {
|
||||
backgroundColor: "rgb(0 0 0 / 0.7)",
|
||||
},
|
||||
},
|
||||
[navigate, user]
|
||||
);
|
||||
|
||||
function handleClose() {
|
||||
setIsDialogOpen(false);
|
||||
setTimeout(() => navigate("/"), theme.transitions.duration.leavingScreen);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isDialogOpen}
|
||||
onClose={handleClose}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
width: "600px",
|
||||
maxWidth: "600px",
|
||||
},
|
||||
}}
|
||||
slotProps={{
|
||||
backdrop: {
|
||||
style: {
|
||||
backgroundColor: "rgb(0 0 0 / 0.7)",
|
||||
},
|
||||
},
|
||||
}}
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={formik.handleSubmit}
|
||||
noValidate
|
||||
sx={{
|
||||
position: "relative",
|
||||
backgroundColor: "white",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexDirection: "column",
|
||||
p: upMd ? "50px" : "18px",
|
||||
pb: upMd ? "40px" : "30px",
|
||||
gap: "15px",
|
||||
borderRadius: "12px",
|
||||
boxShadow: cardShadow,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onClick={handleClose}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
right: "7px",
|
||||
top: "7px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={formik.handleSubmit}
|
||||
noValidate
|
||||
sx={{
|
||||
position: "relative",
|
||||
backgroundColor: "white",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexDirection: "column",
|
||||
p: upMd ? "50px" : "18px",
|
||||
pb: upMd ? "40px" : "30px",
|
||||
gap: "15px",
|
||||
borderRadius: "12px",
|
||||
boxShadow: cardShadow,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onClick={handleClose}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
right: "7px",
|
||||
top: "7px",
|
||||
}}
|
||||
>
|
||||
<CloseIcon sx={{ transform: "scale(1.5)" }} />
|
||||
</IconButton>
|
||||
<Box>
|
||||
<PenaLogo width={upMd ? 233 : 196} color="black" />
|
||||
</Box>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.gray.dark,
|
||||
mt: "5px",
|
||||
mb: upMd ? "10px" : "33px",
|
||||
}}
|
||||
>
|
||||
Вход в личный кабинет
|
||||
</Typography>
|
||||
<InputTextfield
|
||||
TextfieldProps={{
|
||||
value: formik.values.email,
|
||||
placeholder: "username",
|
||||
onBlur: formik.handleBlur,
|
||||
error: formik.touched.email && Boolean(formik.errors.email),
|
||||
helperText: formik.touched.email && formik.errors.email,
|
||||
}}
|
||||
onChange={formik.handleChange}
|
||||
color="#F2F3F7"
|
||||
id="email"
|
||||
label="Email"
|
||||
gap={upMd ? "10px" : "10px"}
|
||||
/>
|
||||
<PasswordInput
|
||||
TextfieldProps={{
|
||||
value: formik.values.password,
|
||||
placeholder: "Не менее 8 символов",
|
||||
onBlur: formik.handleBlur,
|
||||
error: formik.touched.password && Boolean(formik.errors.password),
|
||||
helperText: formik.touched.password && formik.errors.password,
|
||||
type: "password",
|
||||
}}
|
||||
onChange={formik.handleChange}
|
||||
color="#F2F3F7"
|
||||
id="password"
|
||||
label="Пароль"
|
||||
gap={upMd ? "10px" : "10px"}
|
||||
/>
|
||||
<Button
|
||||
variant="pena-contained-dark"
|
||||
fullWidth
|
||||
type="submit"
|
||||
disabled={formik.isSubmitting}
|
||||
sx={{
|
||||
py: "12px",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.purple.dark,
|
||||
},
|
||||
"&:active": {
|
||||
color: "white",
|
||||
backgroundColor: "black",
|
||||
},
|
||||
}}
|
||||
>Войти</Button>
|
||||
{/* <Link
|
||||
<CloseIcon sx={{ transform: "scale(1.5)" }} />
|
||||
</IconButton>
|
||||
<Box>
|
||||
<PenaLogo width={upMd ? 233 : 196} color="black" />
|
||||
</Box>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.gray.dark,
|
||||
mt: "5px",
|
||||
mb: upMd ? "10px" : "33px",
|
||||
}}
|
||||
>
|
||||
Вход в личный кабинет
|
||||
</Typography>
|
||||
<InputTextfield
|
||||
TextfieldProps={{
|
||||
value: formik.values.email,
|
||||
placeholder: "username",
|
||||
onBlur: formik.handleBlur,
|
||||
error: formik.touched.email && Boolean(formik.errors.email),
|
||||
helperText: formik.touched.email && formik.errors.email,
|
||||
}}
|
||||
onChange={formik.handleChange}
|
||||
color="#F2F3F7"
|
||||
id="email"
|
||||
label="Email"
|
||||
gap={upMd ? "10px" : "10px"}
|
||||
/>
|
||||
<PasswordInput
|
||||
TextfieldProps={{
|
||||
value: formik.values.password,
|
||||
placeholder: "Не менее 8 символов",
|
||||
onBlur: formik.handleBlur,
|
||||
error: formik.touched.password && Boolean(formik.errors.password),
|
||||
helperText: formik.touched.password && formik.errors.password,
|
||||
type: "password",
|
||||
}}
|
||||
onChange={formik.handleChange}
|
||||
color="#F2F3F7"
|
||||
id="password"
|
||||
label="Пароль"
|
||||
gap={upMd ? "10px" : "10px"}
|
||||
/>
|
||||
<Button
|
||||
variant="pena-contained-dark"
|
||||
fullWidth
|
||||
type="submit"
|
||||
disabled={formik.isSubmitting}
|
||||
sx={{
|
||||
py: "12px",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.purple.dark,
|
||||
},
|
||||
"&:active": {
|
||||
color: "white",
|
||||
backgroundColor: "black",
|
||||
},
|
||||
}}
|
||||
>
|
||||
Войти
|
||||
</Button>
|
||||
{/* <Link
|
||||
component={RouterLink}
|
||||
to="/"
|
||||
href="#"
|
||||
@ -199,29 +195,31 @@ export default function SigninDialog() {
|
||||
>
|
||||
Забыли пароль?
|
||||
</Link> */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
mt: "auto",
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ color: theme.palette.purple.main, textAlign: "center" }}>
|
||||
Вы еще не присоединились?
|
||||
</Typography>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to="/signup"
|
||||
state={{ backgroundLocation: location.state.backgroundLocation }}
|
||||
sx={{ color: theme.palette.purple.main }}
|
||||
>
|
||||
Регистрация
|
||||
</Link>
|
||||
</Box>
|
||||
<AmoButton/>
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
mt: "auto",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{ color: theme.palette.purple.main, textAlign: "center" }}
|
||||
>
|
||||
Вы еще не присоединились?
|
||||
</Typography>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to="/signup"
|
||||
state={{ backgroundLocation: location.state.backgroundLocation }}
|
||||
sx={{ color: theme.palette.purple.main }}
|
||||
>
|
||||
Регистрация
|
||||
</Link>
|
||||
</Box>
|
||||
<AmoButton />
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
@ -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<boolean>(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<Values>({
|
||||
initialValues,
|
||||
validationSchema,
|
||||
onSubmit: (values, formikHelpers) => {
|
||||
makeRequest<RegisterRequest, RegisterResponse>({
|
||||
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<boolean>(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<Values>({
|
||||
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 (
|
||||
<Dialog
|
||||
open={isDialogOpen}
|
||||
onClose={handleClose}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
width: "600px",
|
||||
maxWidth: "600px",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(
|
||||
function redirectIfSignedIn() {
|
||||
if (user) navigate("/tariffs", { replace: true });
|
||||
}}
|
||||
slotProps={{
|
||||
backdrop: {
|
||||
style: {
|
||||
backgroundColor: "rgb(0 0 0 / 0.7)",
|
||||
},
|
||||
},
|
||||
[navigate, user]
|
||||
);
|
||||
|
||||
function handleClose() {
|
||||
setIsDialogOpen(false);
|
||||
setTimeout(() => navigate("/"), theme.transitions.duration.leavingScreen);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isDialogOpen}
|
||||
onClose={handleClose}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
width: "600px",
|
||||
maxWidth: "600px",
|
||||
},
|
||||
}}
|
||||
slotProps={{
|
||||
backdrop: {
|
||||
style: {
|
||||
backgroundColor: "rgb(0 0 0 / 0.7)",
|
||||
},
|
||||
},
|
||||
}}
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={formik.handleSubmit}
|
||||
noValidate
|
||||
sx={{
|
||||
position: "relative",
|
||||
backgroundColor: "white",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexDirection: "column",
|
||||
p: upMd ? "50px" : "18px",
|
||||
pb: upMd ? "40px" : "30px",
|
||||
gap: "15px",
|
||||
borderRadius: "12px",
|
||||
boxShadow: cardShadow,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onClick={handleClose}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
right: "7px",
|
||||
top: "7px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={formik.handleSubmit}
|
||||
noValidate
|
||||
sx={{
|
||||
position: "relative",
|
||||
backgroundColor: "white",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexDirection: "column",
|
||||
p: upMd ? "50px" : "18px",
|
||||
pb: upMd ? "40px" : "30px",
|
||||
gap: "15px",
|
||||
borderRadius: "12px",
|
||||
boxShadow: cardShadow,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onClick={handleClose}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
right: "7px",
|
||||
top: "7px",
|
||||
}}
|
||||
>
|
||||
<CloseIcon sx={{ transform: "scale(1.5)" }} />
|
||||
</IconButton>
|
||||
<Box sx={{ mt: upMd ? undefined : "62px" }}>
|
||||
<PenaLogo width={upMd ? 233 : 196} color="black" />
|
||||
</Box>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.gray.dark,
|
||||
mt: "5px",
|
||||
mb: upMd ? "35px" : "33px",
|
||||
}}
|
||||
>
|
||||
Регистрация
|
||||
</Typography>
|
||||
<InputTextfield
|
||||
TextfieldProps={{
|
||||
value: formik.values.email,
|
||||
placeholder: "username",
|
||||
onBlur: formik.handleBlur,
|
||||
error: formik.touched.email && Boolean(formik.errors.email),
|
||||
helperText: formik.touched.email && formik.errors.email,
|
||||
}}
|
||||
onChange={formik.handleChange}
|
||||
color="#F2F3F7"
|
||||
id="email"
|
||||
label="Email"
|
||||
gap={upMd ? "10px" : "10px"}
|
||||
/>
|
||||
<PasswordInput
|
||||
TextfieldProps={{
|
||||
value: formik.values.password,
|
||||
placeholder: "Не менее 8 символов",
|
||||
onBlur: formik.handleBlur,
|
||||
error: formik.touched.password && Boolean(formik.errors.password),
|
||||
helperText: formik.touched.password && formik.errors.password,
|
||||
autoComplete: "new-password",
|
||||
}}
|
||||
onChange={formik.handleChange}
|
||||
color="#F2F3F7"
|
||||
id="password"
|
||||
label="Пароль"
|
||||
gap={upMd ? "10px" : "10px"}
|
||||
/>
|
||||
<PasswordInput
|
||||
TextfieldProps={{
|
||||
value: formik.values.repeatPassword,
|
||||
placeholder: "Не менее 8 символов",
|
||||
onBlur: formik.handleBlur,
|
||||
error: formik.touched.repeatPassword && Boolean(formik.errors.repeatPassword),
|
||||
helperText: formik.touched.repeatPassword && formik.errors.repeatPassword,
|
||||
autoComplete: "new-password",
|
||||
}}
|
||||
onChange={formik.handleChange}
|
||||
color="#F2F3F7"
|
||||
id="repeatPassword"
|
||||
label="Повторить пароль"
|
||||
gap={upMd ? "10px" : "10px"}
|
||||
/>
|
||||
<Button
|
||||
variant="pena-contained-dark"
|
||||
fullWidth
|
||||
type="submit"
|
||||
disabled={formik.isSubmitting}
|
||||
sx={{
|
||||
py: "12px",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.purple.dark,
|
||||
},
|
||||
"&:active": {
|
||||
color: "white",
|
||||
backgroundColor: "black",
|
||||
},
|
||||
}}
|
||||
>Зарегистрироваться</Button>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to="/signin"
|
||||
state={{ backgroundLocation: location.state.backgroundLocation }}
|
||||
sx={{
|
||||
color: theme.palette.purple.main,
|
||||
mt: "auto",
|
||||
}}
|
||||
>
|
||||
Вход в личный кабинет
|
||||
</Link>
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
<CloseIcon sx={{ transform: "scale(1.5)" }} />
|
||||
</IconButton>
|
||||
<Box sx={{ mt: upMd ? undefined : "62px" }}>
|
||||
<PenaLogo width={upMd ? 233 : 196} color="black" />
|
||||
</Box>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.gray.dark,
|
||||
mt: "5px",
|
||||
mb: upMd ? "35px" : "33px",
|
||||
}}
|
||||
>
|
||||
Регистрация
|
||||
</Typography>
|
||||
<InputTextfield
|
||||
TextfieldProps={{
|
||||
value: formik.values.email,
|
||||
placeholder: "username",
|
||||
onBlur: formik.handleBlur,
|
||||
error: formik.touched.email && Boolean(formik.errors.email),
|
||||
helperText: formik.touched.email && formik.errors.email,
|
||||
}}
|
||||
onChange={formik.handleChange}
|
||||
color="#F2F3F7"
|
||||
id="email"
|
||||
label="Email"
|
||||
gap={upMd ? "10px" : "10px"}
|
||||
/>
|
||||
<PasswordInput
|
||||
TextfieldProps={{
|
||||
value: formik.values.password,
|
||||
placeholder: "Не менее 8 символов",
|
||||
onBlur: formik.handleBlur,
|
||||
error: formik.touched.password && Boolean(formik.errors.password),
|
||||
helperText: formik.touched.password && formik.errors.password,
|
||||
autoComplete: "new-password",
|
||||
}}
|
||||
onChange={formik.handleChange}
|
||||
color="#F2F3F7"
|
||||
id="password"
|
||||
label="Пароль"
|
||||
gap={upMd ? "10px" : "10px"}
|
||||
/>
|
||||
<PasswordInput
|
||||
TextfieldProps={{
|
||||
value: formik.values.repeatPassword,
|
||||
placeholder: "Не менее 8 символов",
|
||||
onBlur: formik.handleBlur,
|
||||
error:
|
||||
formik.touched.repeatPassword &&
|
||||
Boolean(formik.errors.repeatPassword),
|
||||
helperText:
|
||||
formik.touched.repeatPassword && formik.errors.repeatPassword,
|
||||
autoComplete: "new-password",
|
||||
}}
|
||||
onChange={formik.handleChange}
|
||||
color="#F2F3F7"
|
||||
id="repeatPassword"
|
||||
label="Повторить пароль"
|
||||
gap={upMd ? "10px" : "10px"}
|
||||
/>
|
||||
<Button
|
||||
variant="pena-contained-dark"
|
||||
fullWidth
|
||||
type="submit"
|
||||
disabled={formik.isSubmitting}
|
||||
sx={{
|
||||
py: "12px",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.purple.dark,
|
||||
},
|
||||
"&:active": {
|
||||
color: "white",
|
||||
backgroundColor: "black",
|
||||
},
|
||||
}}
|
||||
>
|
||||
Зарегистрироваться
|
||||
</Button>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to="/signin"
|
||||
state={{ backgroundLocation: location.state.backgroundLocation }}
|
||||
sx={{
|
||||
color: theme.palette.purple.main,
|
||||
mt: "auto",
|
||||
}}
|
||||
>
|
||||
Вход в личный кабинет
|
||||
</Link>
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
@ -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<never, ServiceKeyToPrivilegesMap>({
|
||||
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();
|
||||
}, []);
|
||||
}
|
||||
|
@ -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<never, GetDiscountsResponse>({
|
||||
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();
|
||||
}, []);
|
||||
}
|
||||
|
@ -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<never, GetTariffsResponse>({
|
||||
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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user