Merge branch 'with-kitui-package' into dev
This commit is contained in:
commit
a74473b484
1
.yarnrc
Normal file
1
.yarnrc
Normal file
@ -0,0 +1 @@
|
||||
"@frontend:registry" "https://penahub.gitlab.yandexcloud.net/api/v4/packages/npm/"
|
21683
package-lock.json
generated
21683
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@frontend/kitui": "^1.0.6",
|
||||
"@mui/icons-material": "^5.10.14",
|
||||
"@mui/material": "^5.10.14",
|
||||
"axios": "^1.3.4",
|
||||
@ -21,7 +22,6 @@
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.4.3",
|
||||
"reconnecting-eventsource": "^1.6.2",
|
||||
"web-vitals": "^2.1.0",
|
||||
"yup": "^1.1.1",
|
||||
"zustand": "^4.3.6"
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { authStore } from "@root/stores/makeRequest";
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
|
||||
|
||||
const apiUrl = process.env.NODE_ENV === "production" ? "" : "https://hub.pena.digital";
|
||||
|
||||
const makeRequest = authStore.getState().makeRequest;
|
||||
|
||||
export function logout() {
|
||||
return makeRequest<never, void>({
|
||||
url: apiUrl + "/auth/logout",
|
||||
|
@ -1,138 +0,0 @@
|
||||
import { CreateTicketRequest, CreateTicketResponse, GetMessagesRequest, GetMessagesResponse, GetTicketsRequest, GetTicketsResponse, SendTicketMessageRequest } from "@root/model/ticket";
|
||||
import { authStore } from "@root/stores/makeRequest";
|
||||
import ReconnectingEventSource from "reconnecting-eventsource";
|
||||
|
||||
|
||||
const makeRequest = authStore.getState().makeRequest;
|
||||
|
||||
const supportApiUrl = "https://hub.pena.digital/heruvym";
|
||||
|
||||
export function subscribeToAllTickets({ onMessage, onError, accessToken }: {
|
||||
accessToken: string;
|
||||
onMessage: (e: MessageEvent) => void;
|
||||
onError: (e: Event) => void;
|
||||
}) {
|
||||
const url = `${supportApiUrl}/subscribe?Authorization=${accessToken}`;
|
||||
|
||||
const eventSource = createEventSource(onMessage, onError, url);
|
||||
|
||||
return () => {
|
||||
eventSource.close();
|
||||
};
|
||||
}
|
||||
|
||||
export function subscribeToTicketMessages({ onMessage, onError, accessToken, ticketId }: {
|
||||
accessToken: string;
|
||||
ticketId: string;
|
||||
onMessage: (e: MessageEvent) => void;
|
||||
onError: (e: Event) => void;
|
||||
}) {
|
||||
const url = `${supportApiUrl}/ticket?ticket=${ticketId}&Authorization=${accessToken}`;
|
||||
|
||||
const eventSource = createEventSource(onMessage, onError, url);
|
||||
|
||||
return () => {
|
||||
eventSource.close();
|
||||
};
|
||||
}
|
||||
|
||||
export function subscribeToUnauthTicketMessages({ onMessage, onError, sessionId, ticketId }: {
|
||||
ticketId: string;
|
||||
sessionId: string;
|
||||
onMessage: (e: MessageEvent) => void;
|
||||
onError: (e: Event) => void;
|
||||
}) {
|
||||
const url = `${supportApiUrl}/ticket?ticket=${ticketId}&s=${sessionId}`;
|
||||
|
||||
const eventSource = createEventSource(onMessage, onError, url);
|
||||
|
||||
return () => {
|
||||
eventSource.close();
|
||||
};
|
||||
}
|
||||
|
||||
export function getTickets({ body, signal }: {
|
||||
body: GetTicketsRequest;
|
||||
signal: AbortSignal;
|
||||
}): Promise<GetTicketsResponse> {
|
||||
return makeRequest<GetTicketsRequest, GetTicketsResponse>({
|
||||
url: `${supportApiUrl}/getTickets`,
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
body,
|
||||
signal,
|
||||
});
|
||||
}
|
||||
|
||||
export function getUnauthTicket(signal: AbortSignal): Promise<GetTicketsResponse> {
|
||||
return makeRequest<GetTicketsRequest, GetTicketsResponse>({
|
||||
url: `${supportApiUrl}/getTickets`,
|
||||
method: "POST",
|
||||
useToken: false,
|
||||
body: {
|
||||
amt: 1,
|
||||
page: 0,
|
||||
status: "open",
|
||||
},
|
||||
signal,
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function getTicketMessages({ body, signal }: {
|
||||
body: GetMessagesRequest;
|
||||
signal: AbortSignal;
|
||||
}): Promise<GetMessagesResponse> {
|
||||
return makeRequest({
|
||||
url: `${supportApiUrl}/getMessages`,
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
body,
|
||||
signal,
|
||||
});
|
||||
}
|
||||
|
||||
export function getUnauthTicketMessages({ body, signal }: {
|
||||
body: GetMessagesRequest;
|
||||
signal: AbortSignal;
|
||||
}): Promise<GetMessagesResponse> {
|
||||
return makeRequest({
|
||||
url: `${supportApiUrl}/getMessages`,
|
||||
method: "POST",
|
||||
useToken: false,
|
||||
body,
|
||||
signal,
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function sendTicketMessage(body: SendTicketMessageRequest, withCredentials?: boolean) {
|
||||
return makeRequest({
|
||||
url: `${supportApiUrl}/send`,
|
||||
method: "POST",
|
||||
useToken: !withCredentials,
|
||||
body,
|
||||
withCredentials,
|
||||
});
|
||||
}
|
||||
|
||||
export function createTicket(body: CreateTicketRequest, useToken = true): Promise<CreateTicketResponse> {
|
||||
return makeRequest({
|
||||
url: `${supportApiUrl}/create`,
|
||||
method: "POST",
|
||||
useToken,
|
||||
body,
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
|
||||
function createEventSource(onMessage: (e: MessageEvent) => void, onError: (e: Event) => void, url: string) {
|
||||
const eventSource = new ReconnectingEventSource(url);
|
||||
|
||||
eventSource.addEventListener("open", () => console.log(`EventSource connected with ${url}`));
|
||||
eventSource.addEventListener("close", () => console.log(`EventSource closed with ${url}`));
|
||||
eventSource.addEventListener("message", onMessage);
|
||||
eventSource.addEventListener("error", onError);
|
||||
|
||||
return eventSource;
|
||||
}
|
@ -1,21 +1,9 @@
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { PatchUserRequest, User } from "@root/model/user";
|
||||
import { authStore } from "@root/stores/makeRequest";
|
||||
|
||||
|
||||
const apiUrl = process.env.NODE_ENV === "production" ? "" : "https://hub.pena.digital";
|
||||
|
||||
const makeRequest = authStore.getState().makeRequest;
|
||||
|
||||
export function getUser(userId: string): Promise<User | null> {
|
||||
return makeRequest<never, User>({
|
||||
url: `${apiUrl}/user/${userId}`,
|
||||
contentType: true,
|
||||
method: "GET",
|
||||
useToken: false,
|
||||
withCredentials: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function patchUser(user: PatchUserRequest) {
|
||||
return makeRequest<PatchUserRequest, User>({
|
||||
url: apiUrl + "/user/",
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { Box, FormControl, IconButton, InputAdornment, InputBase, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { createTicket, getUnauthTicketMessages, sendTicketMessage, subscribeToUnauthTicketMessages } from "@root/api/tickets";
|
||||
import { GetMessagesRequest, TicketMessage } from "@root/model/ticket";
|
||||
import { addOrUpdateUnauthMessages, setUnauthTicketFetchState, useUnauthTicketStore, incrementUnauthMessageApiPage, setUnauthIsPreventAutoscroll, setUnauthSessionData, setIsMessageSending } from "@root/stores/unauthTicket";
|
||||
import { TicketMessage } from "@frontend/kitui";
|
||||
import { addOrUpdateUnauthMessages, useUnauthTicketStore, incrementUnauthMessageApiPage, setUnauthIsPreventAutoscroll, setUnauthSessionData, setIsMessageSending } from "@root/stores/unauthTicket";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import ChatMessage from "../ChatMessage";
|
||||
import SendIcon from "../icons/SendIcon";
|
||||
import UserCircleIcon from "./UserCircleIcon";
|
||||
import { throttle } from "@root/utils/decorators";
|
||||
import { getMessageFromFetchError } from "@root/utils/backendMessageHandler";
|
||||
import { throttle } from "@frontend/kitui";
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { useTicketMessages, getMessageFromFetchError, useSSESubscription, useEventListener, createTicket } from "@frontend/kitui";
|
||||
|
||||
|
||||
interface Props {
|
||||
@ -24,93 +24,52 @@ export default function Chat({ sx }: Props) {
|
||||
const messageApiPage = useUnauthTicketStore(state => state.apiPage);
|
||||
const messagesPerPage = useUnauthTicketStore(state => state.messagesPerPage);
|
||||
const isMessageSending = useUnauthTicketStore(state => state.isMessageSending);
|
||||
const messagesFetchStateRef = useRef(useUnauthTicketStore.getState().fetchState);
|
||||
const isPreventAutoscroll = useUnauthTicketStore(state => state.isPreventAutoscroll);
|
||||
const lastMessageId = useUnauthTicketStore(state => state.lastMessageId);
|
||||
const chatBoxRef = useRef<HTMLDivElement>();
|
||||
const chatBoxRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(function fetchTicketMessages() {
|
||||
if (!sessionData) return;
|
||||
const fetchState = useTicketMessages({
|
||||
url: "https://admin.pena.digital/heruvym/getMessages",
|
||||
isUnauth: true,
|
||||
ticketId: sessionData?.ticketId,
|
||||
messagesPerPage,
|
||||
messageApiPage,
|
||||
onNewMessages: 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);
|
||||
}, []),
|
||||
});
|
||||
|
||||
const getTicketsBody: GetMessagesRequest = {
|
||||
amt: messagesPerPage,
|
||||
page: messageApiPage,
|
||||
ticket: sessionData.ticketId,
|
||||
};
|
||||
const controller = new AbortController();
|
||||
|
||||
setUnauthTicketFetchState("fetching");
|
||||
getUnauthTicketMessages({
|
||||
body: getTicketsBody,
|
||||
signal: controller.signal,
|
||||
}).then(result => {
|
||||
console.log("GetMessagesResponse", result);
|
||||
if (result?.length > 0) {
|
||||
if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) chatBoxRef.current.scrollTop = 1;
|
||||
addOrUpdateUnauthMessages(result);
|
||||
setUnauthTicketFetchState("idle");
|
||||
} else setUnauthTicketFetchState("all fetched");
|
||||
}).catch(error => {
|
||||
console.log("Error fetching messages", error);
|
||||
if (error.code !== "ERR_CANCELED") enqueueSnackbar(error.message);
|
||||
});
|
||||
|
||||
return () => {
|
||||
controller.abort();
|
||||
};
|
||||
}, [messageApiPage, messagesPerPage, sessionData]);
|
||||
|
||||
useEffect(function subscribeToMessages() {
|
||||
if (!sessionData) return;
|
||||
|
||||
const unsubscribe = subscribeToUnauthTicketMessages({
|
||||
sessionId: sessionData.sessionId,
|
||||
ticketId: sessionData.ticketId,
|
||||
onMessage(event) {
|
||||
try {
|
||||
const newMessage = JSON.parse(event.data) as TicketMessage;
|
||||
if (!newMessage.id) throw new Error("Bad SSE response");
|
||||
console.log("SSE: parsed newMessage:", newMessage);
|
||||
addOrUpdateUnauthMessages([newMessage]);
|
||||
} catch (error) {
|
||||
console.log("SSE: couldn't parse:", event.data);
|
||||
console.log("Error parsing SSE message", error);
|
||||
}
|
||||
},
|
||||
onError(event) {
|
||||
console.log("SSE Error:", event);
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
useSSESubscription<TicketMessage>({
|
||||
enabled: Boolean(sessionData),
|
||||
url: `https://hub.pena.digital/heruvym/ticket?ticket=${sessionData?.ticketId}&s=${sessionData?.sessionId}`,
|
||||
onNewData: addOrUpdateUnauthMessages,
|
||||
onDisconnect: useCallback(() => {
|
||||
setUnauthIsPreventAutoscroll(false);
|
||||
};
|
||||
}, [sessionData]);
|
||||
|
||||
useEffect(function attachScrollHandler() {
|
||||
if (!chatBoxRef.current) return;
|
||||
}, []),
|
||||
marker: "ticket"
|
||||
});
|
||||
|
||||
const throttledScrollHandler = useMemo(() => throttle(() => {
|
||||
const chatBox = chatBoxRef.current;
|
||||
const scrollHandler = () => {
|
||||
const scrollBottom = chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight;
|
||||
const isPreventAutoscroll = scrollBottom > chatBox.clientHeight;
|
||||
setUnauthIsPreventAutoscroll(isPreventAutoscroll);
|
||||
if (!chatBox) return;
|
||||
|
||||
if (messagesFetchStateRef.current !== "idle") return;
|
||||
const scrollBottom = chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight;
|
||||
const isPreventAutoscroll = scrollBottom > chatBox.clientHeight;
|
||||
setUnauthIsPreventAutoscroll(isPreventAutoscroll);
|
||||
|
||||
if (chatBox.scrollTop < chatBox.clientHeight) {
|
||||
incrementUnauthMessageApiPage();
|
||||
}
|
||||
};
|
||||
if (fetchState !== "idle") return;
|
||||
|
||||
const throttledScrollHandler = throttle(scrollHandler, 200);
|
||||
chatBox.addEventListener("scroll", throttledScrollHandler);
|
||||
if (chatBox.scrollTop < chatBox.clientHeight) {
|
||||
incrementUnauthMessageApiPage();
|
||||
}
|
||||
}, 200), [fetchState]);
|
||||
|
||||
return () => {
|
||||
chatBox.removeEventListener("scroll", throttledScrollHandler);
|
||||
};
|
||||
}, []);
|
||||
useEventListener("scroll", throttledScrollHandler, chatBoxRef);
|
||||
|
||||
useEffect(function scrollOnNewMessage() {
|
||||
if (!chatBoxRef.current) return;
|
||||
@ -123,17 +82,19 @@ export default function Chat({ sx }: Props) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [lastMessageId]);
|
||||
|
||||
useEffect(() => useUnauthTicketStore.subscribe(state => (messagesFetchStateRef.current = state.fetchState)), []);
|
||||
|
||||
async function handleSendMessage() {
|
||||
if (!messageField || isMessageSending) return;
|
||||
|
||||
if (!sessionData) {
|
||||
setIsMessageSending(true);
|
||||
createTicket({
|
||||
Title: "Unauth title",
|
||||
Message: messageField,
|
||||
}, false).then(response => {
|
||||
url: "https://hub.pena.digital/heruvym/create",
|
||||
body: {
|
||||
Title: "Unauth title",
|
||||
Message: messageField,
|
||||
},
|
||||
useToken: false,
|
||||
}).then(response => {
|
||||
setUnauthSessionData({
|
||||
ticketId: response.Ticket,
|
||||
sessionId: response.sess,
|
||||
@ -147,12 +108,18 @@ export default function Chat({ sx }: Props) {
|
||||
});
|
||||
} else {
|
||||
setIsMessageSending(true);
|
||||
sendTicketMessage({
|
||||
ticket: sessionData.ticketId,
|
||||
message: messageField,
|
||||
lang: "ru",
|
||||
files: [],
|
||||
}, true).catch(error => {
|
||||
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(() => {
|
||||
|
@ -4,7 +4,6 @@ import { Box, Button, Container, IconButton, Typography, useTheme } from "@mui/m
|
||||
|
||||
import SectionWrapper from "../SectionWrapper";
|
||||
import { basketStore } from "@stores/BasketStore";
|
||||
import { authStore } from "@stores/makeRequest";
|
||||
|
||||
import LogoutIcon from "../icons/LogoutIcon";
|
||||
import WalletIcon from "../icons/WalletIcon";
|
||||
@ -15,7 +14,8 @@ import Menu from "../Menu";
|
||||
import { logout } from "@root/api/auth";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { clearUser, useUserStore } from "@root/stores/user";
|
||||
import { getMessageFromFetchError } from "@root/utils/backendMessageHandler";
|
||||
import { getMessageFromFetchError } from "@frontend/kitui";
|
||||
import { clearToken } from "@root/stores/auth";
|
||||
|
||||
interface Props {
|
||||
isLoggedIn: boolean;
|
||||
@ -23,7 +23,6 @@ interface Props {
|
||||
|
||||
export default function NavbarFull({ isLoggedIn }: Props) {
|
||||
const theme = useTheme();
|
||||
const { clearToken } = authStore();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const user = useUserStore((state) => state.user);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useCallback } from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { BrowserRouter, Navigate, Route, Routes, useLocation } from "react-router-dom";
|
||||
import { CssBaseline, ThemeProvider } from "@mui/material";
|
||||
@ -20,26 +20,24 @@ import reportWebVitals from "./reportWebVitals";
|
||||
import { SnackbarProvider, enqueueSnackbar } from "notistack";
|
||||
import "./index.css";
|
||||
import Layout from "./components/Layout";
|
||||
import { getUser } from "./api/user";
|
||||
import { setUser, useUserStore } from "./stores/user";
|
||||
import TariffConstructor from "./pages/TariffConstructor/TariffConstructor";
|
||||
import { useUser } from "./utils/hooks/useUser";
|
||||
|
||||
|
||||
const App = () => {
|
||||
const location = useLocation();
|
||||
const userId = useUserStore(state => state.userId);
|
||||
|
||||
useEffect(function fetchUserData() {
|
||||
if (!userId) return;
|
||||
|
||||
getUser(userId).then(result => {
|
||||
setUser(result);
|
||||
}).catch(error => {
|
||||
console.log("Error fetching user", error);
|
||||
useUser({
|
||||
url: `https://hub.pena.digital/user/${userId}`,
|
||||
userId,
|
||||
onNewUser: setUser,
|
||||
onError: useCallback(error => {
|
||||
enqueueSnackbar(error.response?.data?.message ?? error.message ?? "Error fetching user");
|
||||
setUser(null);
|
||||
});
|
||||
}, [userId]);
|
||||
}, [])
|
||||
});
|
||||
|
||||
if (location.state?.redirectTo) return <Navigate to={location.state.redirectTo} replace state={{ backgroundLocation: location }} />;
|
||||
|
||||
|
@ -1,67 +0,0 @@
|
||||
|
||||
|
||||
export interface CreateTicketRequest {
|
||||
Title: string;
|
||||
Message: string;
|
||||
};
|
||||
|
||||
export interface CreateTicketResponse {
|
||||
Ticket: string;
|
||||
sess: string;
|
||||
};
|
||||
|
||||
export interface SendTicketMessageRequest {
|
||||
message: string;
|
||||
ticket: string;
|
||||
lang: string;
|
||||
files: string[];
|
||||
};
|
||||
|
||||
export type TicketStatus = "open"; // TODO
|
||||
|
||||
export interface GetTicketsRequest {
|
||||
amt: number;
|
||||
/** Пагинация начинается с индекса 0 */
|
||||
page: number;
|
||||
srch?: string;
|
||||
status?: TicketStatus;
|
||||
};
|
||||
|
||||
export interface GetTicketsResponse {
|
||||
count: number;
|
||||
data: Ticket[] | null;
|
||||
};
|
||||
|
||||
export interface Ticket {
|
||||
id: string;
|
||||
user: string;
|
||||
sess: string;
|
||||
ans: string;
|
||||
state: string;
|
||||
top_message: TicketMessage;
|
||||
title: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
rate: number;
|
||||
};
|
||||
|
||||
export interface TicketMessage {
|
||||
id: string;
|
||||
ticket_id: string;
|
||||
user_id: string,
|
||||
session_id: string;
|
||||
message: string;
|
||||
files: string[],
|
||||
shown: { [key: string]: number; },
|
||||
request_screenshot: string,
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export interface GetMessagesRequest {
|
||||
amt: number;
|
||||
page: number;
|
||||
srch?: string;
|
||||
ticket: string;
|
||||
};
|
||||
|
||||
export type GetMessagesResponse = TicketMessage[];
|
@ -2,9 +2,9 @@ import { Box, Typography, FormControl, InputBase, useMediaQuery, useTheme } from
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import CustomButton from "@components/CustomButton";
|
||||
import { createTicket } from "@root/api/tickets";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { cardShadow } from "@root/utils/themes/shadow";
|
||||
import { createTicket } from "@frontend/kitui";
|
||||
|
||||
|
||||
export default function CreateTicket() {
|
||||
@ -18,8 +18,11 @@ export default function CreateTicket() {
|
||||
if (!ticketBodyField || !ticketNameField) return;
|
||||
|
||||
createTicket({
|
||||
Title: ticketNameField,
|
||||
Message: ticketBodyField,
|
||||
url: "https://hub.pena.digital/heruvym/create",
|
||||
body: {
|
||||
Title: ticketNameField,
|
||||
Message: ticketBodyField,
|
||||
}
|
||||
}).then(result => {
|
||||
navigate(`/support/${result.Ticket}`);
|
||||
}).catch(error => {
|
||||
|
@ -6,12 +6,12 @@ import ComplexNavText from "@components/ComplexNavText";
|
||||
import SupportChat from "./SupportChat";
|
||||
import CreateTicket from "./CreateTicket";
|
||||
import TicketList from "./TicketList/TicketList";
|
||||
import { useEffect } from "react";
|
||||
import { getTickets, subscribeToAllTickets } from "@root/api/tickets";
|
||||
import { GetTicketsRequest, Ticket } from "@root/model/ticket";
|
||||
import { updateTickets, setTicketCount, clearTickets, useTicketStore, setTicketsFetchState } from "@root/stores/tickets";
|
||||
import { useCallback } from "react";
|
||||
import { Ticket } from "@frontend/kitui";
|
||||
import { updateTickets, setTicketCount, clearTickets, useTicketStore } from "@root/stores/tickets";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { authStore } from "@root/stores/makeRequest";
|
||||
import { useAuthStore } from "@root/stores/auth";
|
||||
import { useSSESubscription, useTickets } from "@frontend/kitui";
|
||||
|
||||
|
||||
export default function Support() {
|
||||
@ -20,61 +20,30 @@ export default function Support() {
|
||||
const ticketId = useParams().ticketId;
|
||||
const ticketApiPage = useTicketStore(state => state.apiPage);
|
||||
const ticketsPerPage = useTicketStore(state => state.ticketsPerPage);
|
||||
const token = authStore(state => state.token);
|
||||
const token = useAuthStore(state => state.token);
|
||||
|
||||
useEffect(function fetchTickets() {
|
||||
const getTicketsBody: GetTicketsRequest = {
|
||||
amt: ticketsPerPage,
|
||||
page: ticketApiPage,
|
||||
status: "open",
|
||||
};
|
||||
const controller = new AbortController();
|
||||
|
||||
setTicketsFetchState("fetching");
|
||||
getTickets({
|
||||
body: getTicketsBody,
|
||||
signal: controller.signal,
|
||||
}).then(result => {
|
||||
console.log("GetTicketsResponse", result);
|
||||
if (result.data) {
|
||||
updateTickets(result.data);
|
||||
setTicketCount(result.count);
|
||||
setTicketsFetchState("idle");
|
||||
} else setTicketsFetchState("all fetched");
|
||||
}).catch(error => {
|
||||
console.log("Error fetching tickets", error);
|
||||
const fetchState = useTickets({
|
||||
url: "https://hub.pena.digital/heruvym/getTickets",
|
||||
ticketsPerPage,
|
||||
ticketApiPage,
|
||||
onNewTickets: useCallback(result => {
|
||||
if (result.data) updateTickets(result.data);
|
||||
setTicketCount(result.count);
|
||||
}, []),
|
||||
onError: useCallback((error: Error) => {
|
||||
enqueueSnackbar(error.message);
|
||||
});
|
||||
}, [])
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, [ticketApiPage, ticketsPerPage]);
|
||||
|
||||
useEffect(function subscribeToTickets() {
|
||||
if (!token) return;
|
||||
|
||||
const unsubscribe = subscribeToAllTickets({
|
||||
accessToken: token,
|
||||
onMessage(event) {
|
||||
try {
|
||||
const newTicket = JSON.parse(event.data) as Ticket;
|
||||
console.log("SSE: parsed newTicket:", newTicket);
|
||||
if ((newTicket as any).count) return; // Костыль пока бэк не починят
|
||||
updateTickets([newTicket]);
|
||||
} catch (error) {
|
||||
console.log("SSE: couldn't parse:", event.data);
|
||||
console.log("Error parsing ticket SSE", error);
|
||||
}
|
||||
},
|
||||
onError(event) {
|
||||
console.log("SSE Error:", event);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
useSSESubscription<Ticket>({
|
||||
enabled: Boolean(token),
|
||||
url: `https://admin.pena.digital/heruvym/subscribe?Authorization=${token}`,
|
||||
onNewData: updateTickets,
|
||||
onDisconnect: useCallback(() => {
|
||||
clearTickets();
|
||||
};
|
||||
}, [token]);
|
||||
}, []),
|
||||
marker: "ticket"
|
||||
});
|
||||
|
||||
return (
|
||||
<SectionWrapper
|
||||
@ -112,7 +81,7 @@ export default function Support() {
|
||||
}}
|
||||
>
|
||||
<CreateTicket />
|
||||
<TicketList />
|
||||
<TicketList fetchState={fetchState} />
|
||||
</Box>
|
||||
)}
|
||||
</SectionWrapper>
|
||||
|
@ -1,18 +1,18 @@
|
||||
import { Box, Fab, FormControl, IconButton, InputAdornment, InputBase, Typography, useMediaQuery, useTheme, } from "@mui/material";
|
||||
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import CustomButton from "@components/CustomButton";
|
||||
import SendIcon from "@components/icons/SendIcon";
|
||||
import { throttle } from "@utils/decorators";
|
||||
import { makeRequest, throttle } from "@frontend/kitui";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useTicketStore } from "@root/stores/tickets";
|
||||
import { addOrUpdateMessages, clearMessageState, incrementMessageApiPage, setIsPreventAutoscroll, setMessageFetchState, useMessageStore } from "@root/stores/messages";
|
||||
import { getTicketMessages, sendTicketMessage, subscribeToTicketMessages } from "@root/api/tickets";
|
||||
import { GetMessagesRequest, TicketMessage } from "@root/model/ticket";
|
||||
import { authStore } from "@root/stores/makeRequest";
|
||||
import { addOrUpdateMessages, clearMessageState, incrementMessageApiPage, setIsPreventAutoscroll, useMessageStore } from "@root/stores/messages";
|
||||
import { TicketMessage } from "@frontend/kitui";
|
||||
import ChatMessage from "@root/components/ChatMessage";
|
||||
import { cardShadow } from "@root/utils/themes/shadow";
|
||||
import { useAuthStore } from "@root/stores/auth";
|
||||
import { getMessageFromFetchError, useEventListener, useSSESubscription, useTicketMessages } from "@frontend/kitui";
|
||||
|
||||
|
||||
export default function SupportChat() {
|
||||
@ -25,93 +25,52 @@ export default function SupportChat() {
|
||||
const lastMessageId = useMessageStore(state => state.lastMessageId);
|
||||
const messagesPerPage = useMessageStore(state => state.messagesPerPage);
|
||||
const isPreventAutoscroll = useMessageStore(state => state.isPreventAutoscroll);
|
||||
const messagesFetchStateRef = useRef(useMessageStore.getState().fetchState);
|
||||
const token = authStore(state => state.token);
|
||||
const token = useAuthStore(state => state.token);
|
||||
const ticketId = useParams().ticketId;
|
||||
const ticket = tickets.find(ticket => ticket.id === ticketId);
|
||||
const chatBoxRef = useRef<HTMLDivElement>();
|
||||
const chatBoxRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(function fetchTicketMessages() {
|
||||
if (!ticketId) return;
|
||||
|
||||
const getTicketsBody: GetMessagesRequest = {
|
||||
amt: messagesPerPage,
|
||||
page: messageApiPage,
|
||||
ticket: ticketId,
|
||||
};
|
||||
const controller = new AbortController();
|
||||
|
||||
setMessageFetchState("fetching");
|
||||
getTicketMessages({
|
||||
body: getTicketsBody,
|
||||
signal: controller.signal,
|
||||
}).then(result => {
|
||||
console.log("GetMessagesResponse", result);
|
||||
if (result?.length > 0) {
|
||||
if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) chatBoxRef.current.scrollTop = 1;
|
||||
addOrUpdateMessages(result);
|
||||
setMessageFetchState("idle");
|
||||
} else setMessageFetchState("all fetched");
|
||||
}).catch(error => {
|
||||
console.log("Error fetching messages", error);
|
||||
const fetchState = useTicketMessages({
|
||||
url: "https://admin.pena.digital/heruvym/getMessages",
|
||||
ticketId,
|
||||
messagesPerPage,
|
||||
messageApiPage,
|
||||
onNewMessages: useCallback(messages => {
|
||||
if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) chatBoxRef.current.scrollTop = 1;
|
||||
addOrUpdateMessages(messages);
|
||||
}, []),
|
||||
onError: useCallback((error: Error) => {
|
||||
enqueueSnackbar(error.message);
|
||||
});
|
||||
}, []),
|
||||
});
|
||||
|
||||
return () => {
|
||||
controller.abort();
|
||||
};
|
||||
}, [messageApiPage, messagesPerPage, ticketId]);
|
||||
|
||||
useEffect(function subscribeToMessages() {
|
||||
if (!ticketId || !token) return;
|
||||
|
||||
const unsubscribe = subscribeToTicketMessages({
|
||||
ticketId: ticketId,
|
||||
accessToken: token,
|
||||
onMessage(event) {
|
||||
try {
|
||||
const newMessage = JSON.parse(event.data) as TicketMessage;
|
||||
console.log("SSE: parsed newMessage:", newMessage);
|
||||
addOrUpdateMessages([newMessage]);
|
||||
} catch (error) {
|
||||
console.log("SSE: couldn't parse:", event.data);
|
||||
console.log("Error parsing message SSE", error);
|
||||
}
|
||||
},
|
||||
onError(event) {
|
||||
console.log("SSE Error:", event);
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
useSSESubscription<TicketMessage>({
|
||||
enabled: Boolean(token) && Boolean(ticketId),
|
||||
url: `https://admin.pena.digital/heruvym/ticket?ticket=${ticketId}&Authorization=${token}`,
|
||||
onNewData: addOrUpdateMessages,
|
||||
onDisconnect: useCallback(() => {
|
||||
clearMessageState();
|
||||
};
|
||||
}, [ticketId, token]);
|
||||
|
||||
useEffect(function attachScrollHandler() {
|
||||
if (!chatBoxRef.current) return;
|
||||
setIsPreventAutoscroll(false);
|
||||
}, []),
|
||||
marker: "ticket message"
|
||||
});
|
||||
|
||||
const throttledScrollHandler = useMemo(() => throttle(() => {
|
||||
const chatBox = chatBoxRef.current;
|
||||
const scrollHandler = () => {
|
||||
const scrollBottom = chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight;
|
||||
const isPreventAutoscroll = scrollBottom > chatBox.clientHeight;
|
||||
setIsPreventAutoscroll(isPreventAutoscroll);
|
||||
if (!chatBox) return;
|
||||
|
||||
if (messagesFetchStateRef.current !== "idle") return;
|
||||
const scrollBottom = chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight;
|
||||
const isPreventAutoscroll = scrollBottom > chatBox.clientHeight * 20;
|
||||
setIsPreventAutoscroll(isPreventAutoscroll);
|
||||
|
||||
if (chatBox.scrollTop < chatBox.clientHeight) {
|
||||
incrementMessageApiPage();
|
||||
}
|
||||
};
|
||||
if (fetchState !== "idle") return;
|
||||
|
||||
const throttledScrollHandler = throttle(scrollHandler, 200);
|
||||
chatBox.addEventListener("scroll", throttledScrollHandler);
|
||||
if (chatBox.scrollTop < chatBox.clientHeight) {
|
||||
incrementMessageApiPage();
|
||||
}
|
||||
}, 200), [fetchState]);
|
||||
|
||||
return () => {
|
||||
chatBox.removeEventListener("scroll", throttledScrollHandler);
|
||||
};
|
||||
}, []);
|
||||
useEventListener("scroll", throttledScrollHandler, chatBoxRef);
|
||||
|
||||
useEffect(function scrollOnNewMessage() {
|
||||
if (!chatBoxRef.current) return;
|
||||
@ -124,18 +83,25 @@ export default function SupportChat() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [lastMessageId]);
|
||||
|
||||
useEffect(() => useMessageStore.subscribe(state => (messagesFetchStateRef.current = state.fetchState)), []);
|
||||
|
||||
function handleSendMessage() {
|
||||
if (!ticket || !messageField) return;
|
||||
|
||||
sendTicketMessage({
|
||||
ticket: ticket.id,
|
||||
message: messageField,
|
||||
lang: "ru",
|
||||
files: [],
|
||||
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);
|
||||
});
|
||||
setMessageField("");
|
||||
}
|
||||
|
||||
function scrollToBottom(behavior?: ScrollBehavior) {
|
||||
|
@ -1,13 +1,16 @@
|
||||
import { CircularProgress, List, ListItem, Box, useTheme, Pagination } from "@mui/material";
|
||||
import TicketCard from "./TicketCard";
|
||||
import { setTicketApiPage, useTicketStore } from "@root/stores/tickets";
|
||||
import { Ticket } from "@root/model/ticket";
|
||||
import { Ticket } from "@frontend/kitui";
|
||||
|
||||
|
||||
export default function TicketList() {
|
||||
interface Props {
|
||||
fetchState: "fetching" | "idle" | "all fetched";
|
||||
}
|
||||
|
||||
export default function TicketList({ fetchState }: Props) {
|
||||
const theme = useTheme();
|
||||
const tickets = useTicketStore(state => state.tickets);
|
||||
const ticketsFetchState = useTicketStore(state => state.fetchState);
|
||||
const ticketCount = useTicketStore(state => state.ticketCount);
|
||||
const ticketApiPage = useTicketStore(state => state.apiPage);
|
||||
const ticketsPerPage = useTicketStore(state => state.ticketsPerPage);
|
||||
@ -25,10 +28,11 @@ export default function TicketList() {
|
||||
<List
|
||||
sx={{
|
||||
p: 0,
|
||||
minHeight: "120px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "40px",
|
||||
opacity: ticketsFetchState === "fetching" ? 0.4 : 1,
|
||||
opacity: fetchState === "fetching" ? 0.4 : 1,
|
||||
transitionProperty: "opacity",
|
||||
transitionDuration: "200ms",
|
||||
}}
|
||||
@ -43,13 +47,13 @@ export default function TicketList() {
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
{ticketsFetchState === "fetching" && (
|
||||
{fetchState === "fetching" && (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
minHeight: "80px",
|
||||
minHeight: "120px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
|
@ -3,13 +3,13 @@ import SectionWrapper from "@components/SectionWrapper";
|
||||
import ComplexNavText from "@root/components/ComplexNavText";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { setCustomTariffs, useCustomTariffsStore } from "@root/stores/customTariffs";
|
||||
import { useEffect } from "react";
|
||||
import { useCallback } from "react";
|
||||
import Summary from "./Summary";
|
||||
import { fetchCustomTariffs } from "@root/api/tariff";
|
||||
import { getMessageFromFetchError } from "@root/utils/backendMessageHandler";
|
||||
import { getMessageFromFetchError } from "@frontend/kitui";
|
||||
import ComplexHeader from "@root/components/ComplexHeader";
|
||||
import CustomTariffCard from "./CustomTariffCard";
|
||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||
import { useCustomTariffs } from "@root/utils/hooks/useCustomTariffs";
|
||||
|
||||
|
||||
export default function TariffConstructor() {
|
||||
@ -17,15 +17,14 @@ export default function TariffConstructor() {
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const customTariffs = useCustomTariffsStore(state => state.customTariffs);
|
||||
|
||||
useEffect(function getCustomTariffs() {
|
||||
const controller = new AbortController();
|
||||
fetchCustomTariffs(controller.signal).then(setCustomTariffs).catch(error => {
|
||||
useCustomTariffs({
|
||||
url: "https://admin.pena.digital/strator/privilege/service",
|
||||
onNewUser: setCustomTariffs,
|
||||
onError: useCallback(error => {
|
||||
const errorMessage = getMessageFromFetchError(error, "Не удалось получить кастомные тарифы");
|
||||
if (errorMessage) enqueueSnackbar(errorMessage);
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, []);
|
||||
}, [])
|
||||
});
|
||||
|
||||
return (
|
||||
<SectionWrapper
|
||||
|
@ -2,7 +2,6 @@ import { Box, Dialog, IconButton, Link, Typography, useMediaQuery, useTheme } fr
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { useFormik } from "formik";
|
||||
import { authStore } from "@stores/makeRequest";
|
||||
import CustomButton from "@components/CustomButton";
|
||||
import InputTextfield from "@components/InputTextfield";
|
||||
import PenaLogo from "@components/PenaLogo";
|
||||
@ -12,7 +11,8 @@ import { object, string } from "yup";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LoginRequest, LoginResponse } from "@root/model/auth";
|
||||
import { setUserId, useUserStore } from "@root/stores/user";
|
||||
import { getMessageFromFetchError } from "@root/utils/backendMessageHandler";
|
||||
import { getMessageFromFetchError } from "@frontend/kitui";
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { cardShadow } from "@root/utils/themes/shadow";
|
||||
|
||||
interface Values {
|
||||
@ -36,7 +36,6 @@ export default function SigninDialog() {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const navigate = useNavigate();
|
||||
const makeRequest = authStore((state) => state.makeRequest);
|
||||
const location = useLocation();
|
||||
const formik = useFormik<Values>({
|
||||
initialValues,
|
||||
|
@ -6,13 +6,13 @@ import CustomButton from "@components/CustomButton";
|
||||
import InputTextfield from "@components/InputTextfield";
|
||||
import PenaLogo from "@components/PenaLogo";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { authStore } from "@stores/makeRequest";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { object, ref, string } from "yup";
|
||||
import { useEffect, useState } from "react";
|
||||
import { RegisterRequest, RegisterResponse } from "@root/model/auth";
|
||||
import { setUserId, useUserStore } from "@root/stores/user";
|
||||
import { getMessageFromFetchError } from "@root/utils/backendMessageHandler";
|
||||
import { getMessageFromFetchError } from "@frontend/kitui";
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { cardShadow } from "@root/utils/themes/shadow";
|
||||
|
||||
|
||||
@ -40,7 +40,6 @@ export default function SignupDialog() {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const navigate = useNavigate();
|
||||
const makeRequest = authStore(state => state.makeRequest);
|
||||
const location = useLocation();
|
||||
const formik = useFormik<Values>({
|
||||
initialValues,
|
||||
|
24
src/stores/auth.ts
Normal file
24
src/stores/auth.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
|
||||
interface AuthStore {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
token: "",
|
||||
}),
|
||||
{
|
||||
name: "token",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const getToken = () => useAuthStore.getState().token;
|
||||
|
||||
export const setToken = (token: string) => useAuthStore.setState({ token });
|
||||
|
||||
export const clearToken = () => useAuthStore.setState({ token: "" });
|
@ -1,106 +0,0 @@
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
type Token = string;
|
||||
|
||||
interface AuthStore {
|
||||
token: Token;
|
||||
makeRequest: <TRequest, TResponse>(props: FirstRequest<TRequest>) => Promise<TResponse>;
|
||||
clearToken: () => void;
|
||||
}
|
||||
|
||||
interface FirstRequest<T> {
|
||||
method?: string;
|
||||
url: string;
|
||||
body?: T;
|
||||
/** Send access token */
|
||||
useToken?: boolean;
|
||||
contentType?: boolean;
|
||||
signal?: AbortSignal;
|
||||
/** Send refresh token */
|
||||
withCredentials?: boolean;
|
||||
}
|
||||
|
||||
export const authStore = create<AuthStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
token: "",
|
||||
makeRequest: <TRequest, TResponse>(props: FirstRequest<TRequest>): Promise<TResponse> => {
|
||||
const newProps = { ...props, HC: (newToken: Token) => set({ token: newToken }), token: get().token };
|
||||
|
||||
return makeRequest<TRequest, TResponse>(newProps);
|
||||
},
|
||||
clearToken: () => set({ token: "" }),
|
||||
}),
|
||||
{
|
||||
name: "token",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
interface MakeRequest<T> extends FirstRequest<T> {
|
||||
HC: (newToken: Token) => void;
|
||||
token: Token;
|
||||
}
|
||||
|
||||
async function makeRequest<TRequest, TResponse>({
|
||||
method = "post",
|
||||
url,
|
||||
body,
|
||||
useToken = true,
|
||||
contentType = false,
|
||||
HC,
|
||||
token,
|
||||
signal,
|
||||
withCredentials,
|
||||
}: MakeRequest<TRequest>) {
|
||||
//В случае 401 рефреш должен попробовать вызваться 1 раз
|
||||
let headers: Record<string, string> = {};
|
||||
if (useToken) headers["Authorization"] = `Bearer ${token}`;
|
||||
if (contentType) headers["Content-Type"] = "application/json";
|
||||
|
||||
try {
|
||||
const response = await axios<TRequest, AxiosResponse<TResponse & { accessToken?: string }>>({
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
data: body,
|
||||
signal,
|
||||
withCredentials,
|
||||
});
|
||||
|
||||
if (response.data?.accessToken) {
|
||||
HC(response.data.accessToken);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 401 && !withCredentials) {
|
||||
const refreshResponse = await refresh(token);
|
||||
if (refreshResponse.data?.accessToken) HC(refreshResponse.data.accessToken);
|
||||
|
||||
headers["Authorization"] = refreshResponse.data.accessToken;
|
||||
const response = await axios.request<TRequest, AxiosResponse<TResponse>>({
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
data: body,
|
||||
signal,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function refresh(token: Token) {
|
||||
return axios<never, AxiosResponse<{ accessToken: string }>>("https://admin.pena.digital/auth/refresh", {
|
||||
headers: {
|
||||
Authorization: token,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
import { TicketMessage } from "@root/model/ticket";
|
||||
import { TicketMessage } from "@frontend/kitui";
|
||||
import { create } from "zustand";
|
||||
import { devtools } from "zustand/middleware";
|
||||
|
||||
|
||||
interface MessageStore {
|
||||
messages: TicketMessage[];
|
||||
fetchState: "idle" | "fetching" | "all fetched";
|
||||
apiPage: number;
|
||||
messagesPerPage: number;
|
||||
lastMessageId: string | undefined;
|
||||
@ -17,7 +16,6 @@ export const useMessageStore = create<MessageStore>()(
|
||||
(set, get) => ({
|
||||
messages: [],
|
||||
messagesPerPage: 10,
|
||||
fetchState: "idle",
|
||||
apiPage: 0,
|
||||
lastMessageId: undefined,
|
||||
isPreventAutoscroll: false,
|
||||
@ -46,11 +44,8 @@ export const clearMessageState = () => useMessageStore.setState({
|
||||
messages: [],
|
||||
apiPage: 0,
|
||||
lastMessageId: undefined,
|
||||
fetchState: "idle",
|
||||
});
|
||||
|
||||
export const setMessageFetchState = (fetchState: MessageStore["fetchState"]) => useMessageStore.setState({ fetchState });
|
||||
|
||||
export const incrementMessageApiPage = () => {
|
||||
const state = useMessageStore.getState();
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Ticket } from "@root/model/ticket";
|
||||
import { Ticket } from "@frontend/kitui";
|
||||
import { create } from "zustand";
|
||||
import { devtools } from "zustand/middleware";
|
||||
|
||||
@ -6,7 +6,6 @@ import { devtools } from "zustand/middleware";
|
||||
interface TicketStore {
|
||||
ticketCount: number;
|
||||
tickets: Ticket[];
|
||||
fetchState: "idle" | "fetching" | "all fetched";
|
||||
apiPage: number;
|
||||
ticketsPerPage: number;
|
||||
}
|
||||
@ -28,8 +27,6 @@ export const useTicketStore = create<TicketStore>()(
|
||||
|
||||
export const setTicketCount = (ticketCount: number) => useTicketStore.setState({ ticketCount });
|
||||
|
||||
export const setTicketsFetchState = (fetchState: TicketStore["fetchState"]) => useTicketStore.setState({ fetchState });
|
||||
|
||||
export const setTicketApiPage = (apiPage: number) => useTicketStore.setState({ apiPage: apiPage });
|
||||
|
||||
export const updateTickets = (receivedTickets: Ticket[]) => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { TicketMessage } from "@root/model/ticket";
|
||||
import { TicketMessage } from "@frontend/kitui";
|
||||
import { create } from "zustand";
|
||||
import { createJSONStorage, devtools, persist } from "zustand/middleware";
|
||||
|
||||
@ -10,7 +10,6 @@ interface UnauthTicketStore {
|
||||
} | null;
|
||||
isMessageSending: boolean;
|
||||
messages: TicketMessage[];
|
||||
fetchState: "idle" | "fetching" | "all fetched";
|
||||
apiPage: number;
|
||||
messagesPerPage: number;
|
||||
lastMessageId: string | undefined;
|
||||
@ -24,7 +23,6 @@ export const useUnauthTicketStore = create<UnauthTicketStore>()(
|
||||
sessionData: null,
|
||||
isMessageSending: false,
|
||||
messages: [],
|
||||
fetchState: "idle",
|
||||
apiPage: 0,
|
||||
messagesPerPage: 10,
|
||||
lastMessageId: undefined,
|
||||
@ -62,8 +60,6 @@ export const addOrUpdateUnauthMessages = (receivedMessages: TicketMessage[]) =>
|
||||
});
|
||||
};
|
||||
|
||||
export const setUnauthTicketFetchState = (fetchState: UnauthTicketStore["fetchState"]) => useUnauthTicketStore.setState({ fetchState });
|
||||
|
||||
export const incrementUnauthMessageApiPage = () => {
|
||||
const state = useUnauthTicketStore.getState();
|
||||
|
||||
|
@ -1,28 +0,0 @@
|
||||
import { isAxiosError } from "axios";
|
||||
|
||||
|
||||
const backendErrorMessage: Record<string, string> = {
|
||||
"user not found": "Пользователь не найден",
|
||||
"invalid password": "Неправильный пароль",
|
||||
"field <password> is empty": "Поле \"Пароль\" не заполнено",
|
||||
"field <login> is empty": "Поле \"Логин\" не заполнено",
|
||||
"field <email> is empty": "Поле \"E-mail\" не заполнено",
|
||||
"field <phoneNumber> is empty": "Поле \"Номер телефона\" не заполнено",
|
||||
"user with this email or login is exist": "Пользователь уже существует",
|
||||
};
|
||||
|
||||
export function getMessageFromFetchError(error: any, defaultMessage?: string): string | null {
|
||||
if (process.env.NODE_ENV !== "production") console.log(error);
|
||||
|
||||
const message = backendErrorMessage[error.response?.data?.message];
|
||||
if (message) return message;
|
||||
|
||||
if (isAxiosError(error)) {
|
||||
switch (error.code) {
|
||||
case "ERR_NETWORK": return "Ошибка сети";
|
||||
case "ERR_CANCELED": return null;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultMessage ?? "Что-то пошло не так. Повторите попытку позже";
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
type ThrottledFunction<T extends (...args: any) => any> = (...args: Parameters<T>) => void;
|
||||
|
||||
export function throttle<T extends (...args: any) => any>(func: T, ms: number): ThrottledFunction<T> {
|
||||
let isThrottled = false;
|
||||
let savedArgs: Parameters<T> | null;
|
||||
let savedThis: any;
|
||||
|
||||
function wrapper(this: any, ...args: Parameters<T>) {
|
||||
if (isThrottled) {
|
||||
savedArgs = args;
|
||||
savedThis = this;
|
||||
return;
|
||||
}
|
||||
|
||||
func.apply(this, args);
|
||||
|
||||
isThrottled = true;
|
||||
|
||||
setTimeout(function () {
|
||||
isThrottled = false;
|
||||
if (savedArgs) {
|
||||
wrapper.apply(savedThis, savedArgs);
|
||||
savedArgs = savedThis = null;
|
||||
}
|
||||
}, ms);
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
}
|
27
src/utils/hooks/useCustomTariffs.ts
Normal file
27
src/utils/hooks/useCustomTariffs.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { devlog } from "@frontend/kitui";
|
||||
import { CustomTariffsMap } from "@root/model/customTariffs";
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { useEffect } from "react";
|
||||
|
||||
|
||||
export function useCustomTariffs({ onError, onNewUser, url }: {
|
||||
url: string;
|
||||
onNewUser: (response: CustomTariffsMap) => void;
|
||||
onError: (error: any) => void;
|
||||
}) {
|
||||
useEffect(function fetchUserData() {
|
||||
const controller = new AbortController();
|
||||
|
||||
axios<never, AxiosResponse<CustomTariffsMap>>({
|
||||
url,
|
||||
signal: controller.signal,
|
||||
}).then(result => {
|
||||
onNewUser(result.data);
|
||||
}).catch(error => {
|
||||
devlog("Error fetching custom tariffs", error);
|
||||
onError(error);
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, [onError, onNewUser, url]);
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
|
||||
export function useDebounce<T>(value: T, delay: number) {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
return () => clearTimeout(handler);
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
|
||||
export function useThrottle<T>(value: T, delay: number) {
|
||||
const [throttledValue, setThrottledValue] = useState<T>(value);
|
||||
const time = useRef<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
const now = Date.now();
|
||||
if (now > time.current + delay) {
|
||||
time.current = now;
|
||||
setThrottledValue(value);
|
||||
} else {
|
||||
const handler = setTimeout(() => {
|
||||
setThrottledValue(value);
|
||||
}, delay);
|
||||
return () => clearTimeout(handler);
|
||||
}
|
||||
}, [value, delay]);
|
||||
|
||||
return throttledValue;
|
||||
}
|
33
src/utils/hooks/useUser.ts
Normal file
33
src/utils/hooks/useUser.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { devlog, makeRequest } from "@frontend/kitui";
|
||||
import { User } from "@root/model/user";
|
||||
import { useEffect } from "react";
|
||||
|
||||
|
||||
export function useUser({ onError, onNewUser, url, userId }: {
|
||||
url: string;
|
||||
userId: string | null;
|
||||
onNewUser: (response: User) => void;
|
||||
onError: (error: any) => void;
|
||||
}) {
|
||||
useEffect(function fetchUserData() {
|
||||
if (!userId) return;
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
makeRequest<never, User>({
|
||||
url,
|
||||
contentType: true,
|
||||
method: "GET",
|
||||
useToken: false,
|
||||
withCredentials: false,
|
||||
signal: controller.signal,
|
||||
}).then(result => {
|
||||
onNewUser(result);
|
||||
}).catch(error => {
|
||||
devlog("Error fetching user", error);
|
||||
onError(error);
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, [onError, onNewUser, url, userId]);
|
||||
}
|
@ -1,16 +1,16 @@
|
||||
import { useAuthStore } from "@root/stores/auth";
|
||||
import * as React from "react";
|
||||
import { useLocation, Navigate } from "react-router-dom";
|
||||
import { authStore } from "@stores/makeRequest";
|
||||
|
||||
type Props = { children: JSX.Element };
|
||||
type Props = { children: JSX.Element; };
|
||||
|
||||
export default ({ children }: Props) => {
|
||||
const location = useLocation();
|
||||
const { token } = authStore();
|
||||
console.log(token);
|
||||
//Если пользователь авторизован, перенаправляем его в приложение. Иначе пускаем куда хотел
|
||||
if (token) {
|
||||
return <Navigate to="/settings" state={{ from: location }} />;
|
||||
}
|
||||
return children;
|
||||
const location = useLocation();
|
||||
const token = useAuthStore(state => state.token);
|
||||
console.log(token);
|
||||
//Если пользователь авторизован, перенаправляем его в приложение. Иначе пускаем куда хотел
|
||||
if (token) {
|
||||
return <Navigate to="/settings" state={{ from: location }} />;
|
||||
}
|
||||
return children;
|
||||
};
|
||||
|
25
yarn.lock
25
yarn.lock
@ -1450,6 +1450,13 @@
|
||||
minimatch "^3.1.2"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@frontend/kitui@^1.0.6":
|
||||
version "1.0.6"
|
||||
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.6.tgz#0ac4b0e76163de95af3668930e7682a021e83366"
|
||||
integrity sha1-CsSw52Fj3pWvNmiTDnaCoCHoM2Y=
|
||||
dependencies:
|
||||
reconnecting-eventsource "^1.6.2"
|
||||
|
||||
"@humanwhocodes/config-array@^0.11.6":
|
||||
version "0.11.7"
|
||||
resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz"
|
||||
@ -3234,7 +3241,7 @@ async@^3.2.3:
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||
|
||||
at-least-node@^1.0.0:
|
||||
@ -3797,7 +3804,7 @@ colorette@^2.0.10:
|
||||
|
||||
combined-stream@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
@ -4278,7 +4285,7 @@ defined@^1.0.0:
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||
|
||||
depd@2.0.0:
|
||||
@ -5180,7 +5187,7 @@ flatted@^3.1.0:
|
||||
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.15.0:
|
||||
version "1.15.2"
|
||||
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
|
||||
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
|
||||
|
||||
for-each@^0.3.3:
|
||||
@ -5220,7 +5227,7 @@ form-data@^3.0.0:
|
||||
|
||||
form-data@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
|
||||
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
@ -7341,12 +7348,12 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5:
|
||||
|
||||
mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
|
||||
version "1.52.0"
|
||||
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
dependencies:
|
||||
mime-db "1.52.0"
|
||||
@ -8504,7 +8511,7 @@ proxy-addr@~2.0.7:
|
||||
|
||||
proxy-from-env@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
|
||||
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
||||
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||
|
||||
psl@^1.1.33:
|
||||
@ -8783,7 +8790,7 @@ readdirp@~3.6.0:
|
||||
|
||||
reconnecting-eventsource@^1.6.2:
|
||||
version "1.6.2"
|
||||
resolved "https://registry.npmjs.org/reconnecting-eventsource/-/reconnecting-eventsource-1.6.2.tgz"
|
||||
resolved "https://registry.yarnpkg.com/reconnecting-eventsource/-/reconnecting-eventsource-1.6.2.tgz#b7f5b03b1c76291f6fbcb0203004892a57ae253b"
|
||||
integrity sha512-vHhoxVLbA2YcfljWMKEbgR1KVTgwIrnyh/bzVJc+gfQbGcUIToLL6jNhkUL4E+9FbnAcfUVNLIw2YCiliTg/4g==
|
||||
|
||||
recursive-readdir@^2.2.2:
|
||||
|
Loading…
Reference in New Issue
Block a user