use package
This commit is contained in:
parent
5a340d6be6
commit
7faf0cc4b3
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": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.10.5",
|
||||||
"@emotion/styled": "^11.10.5",
|
"@emotion/styled": "^11.10.5",
|
||||||
|
"@frontend/kitui": "^1.0.2",
|
||||||
"@mui/icons-material": "^5.10.14",
|
"@mui/icons-material": "^5.10.14",
|
||||||
"@mui/material": "^5.10.14",
|
"@mui/material": "^5.10.14",
|
||||||
"axios": "^1.3.4",
|
"axios": "^1.3.4",
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import { authStore } from "@root/stores/makeRequest";
|
import { makeRequest } from "./makeRequest";
|
||||||
|
|
||||||
|
|
||||||
const apiUrl = process.env.NODE_ENV === "production" ? "" : "https://hub.pena.digital";
|
const apiUrl = process.env.NODE_ENV === "production" ? "" : "https://hub.pena.digital";
|
||||||
|
|
||||||
const makeRequest = authStore.getState().makeRequest;
|
|
||||||
|
|
||||||
export function logout() {
|
export function logout() {
|
||||||
return makeRequest<never, void>({
|
return makeRequest<never, void>({
|
||||||
url: apiUrl + "/auth/logout",
|
url: apiUrl + "/auth/logout",
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import { CustomTariffsMap } from "@root/model/customTariffs";
|
|
||||||
import axios, { AxiosResponse } from "axios";
|
|
||||||
|
|
||||||
|
|
||||||
export async function fetchCustomTariffs(signal: AbortSignal) {
|
|
||||||
const response = await axios<never, AxiosResponse<CustomTariffsMap>>({
|
|
||||||
url: "https://admin.pena.digital/strator/privilege/service",
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
}
|
|
5
src/api/makeRequest.ts
Normal file
5
src/api/makeRequest.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { createMakeRequest } from "@frontend/kitui";
|
||||||
|
import { getToken, setToken } from "@root/stores/auth";
|
||||||
|
|
||||||
|
|
||||||
|
export const makeRequest = createMakeRequest(getToken, setToken);
|
@ -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 { PatchUserRequest, User } from "@root/model/user";
|
import { PatchUserRequest, User } from "@root/model/user";
|
||||||
import { authStore } from "@root/stores/makeRequest";
|
import { makeRequest } from "./makeRequest";
|
||||||
|
|
||||||
|
|
||||||
const apiUrl = process.env.NODE_ENV === "production" ? "" : "https://hub.pena.digital";
|
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) {
|
export function patchUser(user: PatchUserRequest) {
|
||||||
return makeRequest<PatchUserRequest, User>({
|
return makeRequest<PatchUserRequest, User>({
|
||||||
url: apiUrl + "/user/",
|
url: apiUrl + "/user/",
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
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 { createTicket, getUnauthTicketMessages, sendTicketMessage, subscribeToUnauthTicketMessages } from "@root/api/tickets";
|
import { TicketMessage } from "@frontend/kitui";
|
||||||
import { GetMessagesRequest, TicketMessage } from "@root/model/ticket";
|
import { addOrUpdateUnauthMessages, useUnauthTicketStore, incrementUnauthMessageApiPage, setUnauthIsPreventAutoscroll, setUnauthSessionData, setIsMessageSending } from "@root/stores/unauthTicket";
|
||||||
import { addOrUpdateUnauthMessages, setUnauthTicketFetchState, useUnauthTicketStore, incrementUnauthMessageApiPage, setUnauthIsPreventAutoscroll, setUnauthSessionData, setIsMessageSending } from "@root/stores/unauthTicket";
|
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import ChatMessage from "../ChatMessage";
|
import ChatMessage from "../ChatMessage";
|
||||||
import SendIcon from "../icons/SendIcon";
|
import SendIcon from "../icons/SendIcon";
|
||||||
import UserCircleIcon from "./UserCircleIcon";
|
import UserCircleIcon from "./UserCircleIcon";
|
||||||
import { throttle } from "@root/utils/decorators";
|
import { throttle } from "@frontend/kitui";
|
||||||
import { getMessageFromFetchError } from "@root/utils/backendMessageHandler";
|
import { makeRequest } from "@root/api/makeRequest";
|
||||||
|
import { useTicketMessages, getMessageFromFetchError, useSSESubscription, useEventListener, createTicket } from "@frontend/kitui";
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -24,93 +24,53 @@ export default function Chat({ sx }: Props) {
|
|||||||
const messageApiPage = useUnauthTicketStore(state => state.apiPage);
|
const messageApiPage = useUnauthTicketStore(state => state.apiPage);
|
||||||
const messagesPerPage = useUnauthTicketStore(state => state.messagesPerPage);
|
const messagesPerPage = useUnauthTicketStore(state => state.messagesPerPage);
|
||||||
const isMessageSending = useUnauthTicketStore(state => state.isMessageSending);
|
const isMessageSending = useUnauthTicketStore(state => state.isMessageSending);
|
||||||
const messagesFetchStateRef = useRef(useUnauthTicketStore.getState().fetchState);
|
|
||||||
const isPreventAutoscroll = useUnauthTicketStore(state => state.isPreventAutoscroll);
|
const isPreventAutoscroll = useUnauthTicketStore(state => state.isPreventAutoscroll);
|
||||||
const lastMessageId = useUnauthTicketStore(state => state.lastMessageId);
|
const lastMessageId = useUnauthTicketStore(state => state.lastMessageId);
|
||||||
const chatBoxRef = useRef<HTMLDivElement>();
|
const chatBoxRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(function fetchTicketMessages() {
|
const fetchState = useTicketMessages({
|
||||||
if (!sessionData) return;
|
makeRequest,
|
||||||
|
url: "https://admin.pena.digital/heruvym/getMessages",
|
||||||
const getTicketsBody: GetMessagesRequest = {
|
isUnauth: true,
|
||||||
amt: messagesPerPage,
|
ticketId: sessionData?.ticketId,
|
||||||
page: messageApiPage,
|
messagesPerPage,
|
||||||
ticket: sessionData.ticketId,
|
messageApiPage,
|
||||||
};
|
onNewMessages: useCallback(messages => {
|
||||||
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;
|
if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) chatBoxRef.current.scrollTop = 1;
|
||||||
addOrUpdateUnauthMessages(result);
|
addOrUpdateUnauthMessages(messages);
|
||||||
setUnauthTicketFetchState("idle");
|
}, []),
|
||||||
} else setUnauthTicketFetchState("all fetched");
|
onError: useCallback((error: Error) => {
|
||||||
}).catch(error => {
|
const message = getMessageFromFetchError(error);
|
||||||
console.log("Error fetching messages", error);
|
if (message) enqueueSnackbar(message);
|
||||||
if (error.code !== "ERR_CANCELED") enqueueSnackbar(error.message);
|
}, []),
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
useSSESubscription<TicketMessage>({
|
||||||
controller.abort();
|
enabled: Boolean(sessionData),
|
||||||
};
|
url: `https://hub.pena.digital/heruvym/ticket?ticket=${sessionData?.ticketId}&s=${sessionData?.sessionId}`,
|
||||||
}, [messageApiPage, messagesPerPage, sessionData]);
|
onNewData: addOrUpdateUnauthMessages,
|
||||||
|
onDisconnect: useCallback(() => {
|
||||||
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();
|
|
||||||
setUnauthIsPreventAutoscroll(false);
|
setUnauthIsPreventAutoscroll(false);
|
||||||
};
|
}, []),
|
||||||
}, [sessionData]);
|
marker: "ticket"
|
||||||
|
});
|
||||||
useEffect(function attachScrollHandler() {
|
|
||||||
if (!chatBoxRef.current) return;
|
|
||||||
|
|
||||||
|
const throttledScrollHandler = useMemo(() => throttle(() => {
|
||||||
const chatBox = chatBoxRef.current;
|
const chatBox = chatBoxRef.current;
|
||||||
const scrollHandler = () => {
|
if (!chatBox) return;
|
||||||
|
|
||||||
const scrollBottom = chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight;
|
const scrollBottom = chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight;
|
||||||
const isPreventAutoscroll = scrollBottom > chatBox.clientHeight;
|
const isPreventAutoscroll = scrollBottom > chatBox.clientHeight;
|
||||||
setUnauthIsPreventAutoscroll(isPreventAutoscroll);
|
setUnauthIsPreventAutoscroll(isPreventAutoscroll);
|
||||||
|
|
||||||
if (messagesFetchStateRef.current !== "idle") return;
|
if (fetchState !== "idle") return;
|
||||||
|
|
||||||
if (chatBox.scrollTop < chatBox.clientHeight) {
|
if (chatBox.scrollTop < chatBox.clientHeight) {
|
||||||
incrementUnauthMessageApiPage();
|
incrementUnauthMessageApiPage();
|
||||||
}
|
}
|
||||||
};
|
}, 200), [fetchState]);
|
||||||
|
|
||||||
const throttledScrollHandler = throttle(scrollHandler, 200);
|
useEventListener("scroll", throttledScrollHandler, chatBoxRef);
|
||||||
chatBox.addEventListener("scroll", throttledScrollHandler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
chatBox.removeEventListener("scroll", throttledScrollHandler);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(function scrollOnNewMessage() {
|
useEffect(function scrollOnNewMessage() {
|
||||||
if (!chatBoxRef.current) return;
|
if (!chatBoxRef.current) return;
|
||||||
@ -123,17 +83,20 @@ export default function Chat({ sx }: Props) {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [lastMessageId]);
|
}, [lastMessageId]);
|
||||||
|
|
||||||
useEffect(() => useUnauthTicketStore.subscribe(state => (messagesFetchStateRef.current = state.fetchState)), []);
|
|
||||||
|
|
||||||
async function handleSendMessage() {
|
async function handleSendMessage() {
|
||||||
if (!messageField || isMessageSending) return;
|
if (!messageField || isMessageSending) return;
|
||||||
|
|
||||||
if (!sessionData) {
|
if (!sessionData) {
|
||||||
setIsMessageSending(true);
|
setIsMessageSending(true);
|
||||||
createTicket({
|
createTicket({
|
||||||
|
makeRequest,
|
||||||
|
url: "https://hub.pena.digital/heruvym/create",
|
||||||
|
body: {
|
||||||
Title: "Unauth title",
|
Title: "Unauth title",
|
||||||
Message: messageField,
|
Message: messageField,
|
||||||
}, false).then(response => {
|
},
|
||||||
|
useToken: false,
|
||||||
|
}).then(response => {
|
||||||
setUnauthSessionData({
|
setUnauthSessionData({
|
||||||
ticketId: response.Ticket,
|
ticketId: response.Ticket,
|
||||||
sessionId: response.sess,
|
sessionId: response.sess,
|
||||||
@ -147,12 +110,18 @@ export default function Chat({ sx }: Props) {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setIsMessageSending(true);
|
setIsMessageSending(true);
|
||||||
sendTicketMessage({
|
makeRequest({
|
||||||
|
url: "https://hub.pena.digital/heruvym/send",
|
||||||
|
method: "POST",
|
||||||
|
useToken: false,
|
||||||
|
body: {
|
||||||
ticket: sessionData.ticketId,
|
ticket: sessionData.ticketId,
|
||||||
message: messageField,
|
message: messageField,
|
||||||
lang: "ru",
|
lang: "ru",
|
||||||
files: [],
|
files: [],
|
||||||
}, true).catch(error => {
|
},
|
||||||
|
withCredentials: true,
|
||||||
|
}).catch(error => {
|
||||||
const errorMessage = getMessageFromFetchError(error);
|
const errorMessage = getMessageFromFetchError(error);
|
||||||
if (errorMessage) enqueueSnackbar(errorMessage);
|
if (errorMessage) enqueueSnackbar(errorMessage);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
|
@ -4,7 +4,6 @@ import { Box, Button, Container, IconButton, Typography, useTheme } from "@mui/m
|
|||||||
|
|
||||||
import SectionWrapper from "../SectionWrapper";
|
import SectionWrapper from "../SectionWrapper";
|
||||||
import { basketStore } from "@stores/BasketStore";
|
import { basketStore } from "@stores/BasketStore";
|
||||||
import { authStore } from "@stores/makeRequest";
|
|
||||||
|
|
||||||
import LogoutIcon from "../icons/LogoutIcon";
|
import LogoutIcon from "../icons/LogoutIcon";
|
||||||
import WalletIcon from "../icons/WalletIcon";
|
import WalletIcon from "../icons/WalletIcon";
|
||||||
@ -15,7 +14,8 @@ import Menu from "../Menu";
|
|||||||
import { logout } from "@root/api/auth";
|
import { logout } from "@root/api/auth";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import { clearUser, useUserStore } from "@root/stores/user";
|
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 {
|
interface Props {
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
@ -23,7 +23,6 @@ interface Props {
|
|||||||
|
|
||||||
export default function NavbarFull({ isLoggedIn }: Props) {
|
export default function NavbarFull({ isLoggedIn }: Props) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { clearToken } = authStore();
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const user = useUserStore((state) => state.user);
|
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 ReactDOM from "react-dom/client";
|
||||||
import { BrowserRouter, Navigate, Route, Routes, useLocation } from "react-router-dom";
|
import { BrowserRouter, Navigate, Route, Routes, useLocation } from "react-router-dom";
|
||||||
import { CssBaseline, ThemeProvider } from "@mui/material";
|
import { CssBaseline, ThemeProvider } from "@mui/material";
|
||||||
@ -20,26 +20,26 @@ import reportWebVitals from "./reportWebVitals";
|
|||||||
import { SnackbarProvider, enqueueSnackbar } from "notistack";
|
import { SnackbarProvider, enqueueSnackbar } from "notistack";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import Layout from "./components/Layout";
|
import Layout from "./components/Layout";
|
||||||
import { getUser } from "./api/user";
|
|
||||||
import { setUser, useUserStore } from "./stores/user";
|
import { setUser, useUserStore } from "./stores/user";
|
||||||
import TariffConstructor from "./pages/TariffConstructor/TariffConstructor";
|
import TariffConstructor from "./pages/TariffConstructor/TariffConstructor";
|
||||||
|
import { useUser } from "./utils/hooks/useUser";
|
||||||
|
import { makeRequest } from "./api/makeRequest";
|
||||||
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const userId = useUserStore(state => state.userId);
|
const userId = useUserStore(state => state.userId);
|
||||||
|
|
||||||
useEffect(function fetchUserData() {
|
useUser({
|
||||||
if (!userId) return;
|
makeRequest,
|
||||||
|
url: `https://hub.pena.digital/user/${userId}`,
|
||||||
getUser(userId).then(result => {
|
userId,
|
||||||
setUser(result);
|
onNewUser: setUser,
|
||||||
}).catch(error => {
|
onError: useCallback(error => {
|
||||||
console.log("Error fetching user", error);
|
|
||||||
enqueueSnackbar(error.response?.data?.message ?? error.message ?? "Error fetching user");
|
enqueueSnackbar(error.response?.data?.message ?? error.message ?? "Error fetching user");
|
||||||
setUser(null);
|
setUser(null);
|
||||||
|
}, [])
|
||||||
});
|
});
|
||||||
}, [userId]);
|
|
||||||
|
|
||||||
if (location.state?.redirectTo === "/signin") return <Navigate to="/signin" replace state={{ backgroundLocation: location }} />;
|
if (location.state?.redirectTo === "/signin") return <Navigate to="/signin" replace state={{ backgroundLocation: location }} />;
|
||||||
if (location.state?.redirectTo === "/signup") return <Navigate to="/signup" replace state={{ backgroundLocation: location }} />;
|
if (location.state?.redirectTo === "/signup") return <Navigate to="/signup" 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,8 +2,9 @@ import { Box, Typography, FormControl, InputBase, useMediaQuery, useTheme } from
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import CustomButton from "@components/CustomButton";
|
import CustomButton from "@components/CustomButton";
|
||||||
import { createTicket } from "@root/api/tickets";
|
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
|
import { makeRequest } from "@root/api/makeRequest";
|
||||||
|
import { createTicket } from "@frontend/kitui";
|
||||||
|
|
||||||
|
|
||||||
export default function CreateTicket() {
|
export default function CreateTicket() {
|
||||||
@ -17,8 +18,12 @@ export default function CreateTicket() {
|
|||||||
if (!ticketBodyField || !ticketNameField) return;
|
if (!ticketBodyField || !ticketNameField) return;
|
||||||
|
|
||||||
createTicket({
|
createTicket({
|
||||||
|
makeRequest,
|
||||||
|
url: "https://hub.pena.digital/heruvym/create",
|
||||||
|
body: {
|
||||||
Title: ticketNameField,
|
Title: ticketNameField,
|
||||||
Message: ticketBodyField,
|
Message: ticketBodyField,
|
||||||
|
}
|
||||||
}).then(result => {
|
}).then(result => {
|
||||||
navigate(`/support/${result.Ticket}`);
|
navigate(`/support/${result.Ticket}`);
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
@ -6,12 +6,13 @@ import ComplexNavText from "@components/ComplexNavText";
|
|||||||
import SupportChat from "./SupportChat";
|
import SupportChat from "./SupportChat";
|
||||||
import CreateTicket from "./CreateTicket";
|
import CreateTicket from "./CreateTicket";
|
||||||
import TicketList from "./TicketList/TicketList";
|
import TicketList from "./TicketList/TicketList";
|
||||||
import { useEffect } from "react";
|
import { useCallback } from "react";
|
||||||
import { getTickets, subscribeToAllTickets } from "@root/api/tickets";
|
import { Ticket } from "@frontend/kitui";
|
||||||
import { GetTicketsRequest, Ticket } from "@root/model/ticket";
|
import { updateTickets, setTicketCount, clearTickets, useTicketStore } from "@root/stores/tickets";
|
||||||
import { updateTickets, setTicketCount, clearTickets, useTicketStore, setTicketsFetchState } from "@root/stores/tickets";
|
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import { authStore } from "@root/stores/makeRequest";
|
import { useAuthStore } from "@root/stores/auth";
|
||||||
|
import { makeRequest } from "@root/api/makeRequest";
|
||||||
|
import { useSSESubscription, useTickets } from "@frontend/kitui";
|
||||||
|
|
||||||
|
|
||||||
export default function Support() {
|
export default function Support() {
|
||||||
@ -20,61 +21,31 @@ export default function Support() {
|
|||||||
const ticketId = useParams().ticketId;
|
const ticketId = useParams().ticketId;
|
||||||
const ticketApiPage = useTicketStore(state => state.apiPage);
|
const ticketApiPage = useTicketStore(state => state.apiPage);
|
||||||
const ticketsPerPage = useTicketStore(state => state.ticketsPerPage);
|
const ticketsPerPage = useTicketStore(state => state.ticketsPerPage);
|
||||||
const token = authStore(state => state.token);
|
const token = useAuthStore(state => state.token);
|
||||||
|
|
||||||
useEffect(function fetchTickets() {
|
const fetchState = useTickets({
|
||||||
const getTicketsBody: GetTicketsRequest = {
|
makeRequest,
|
||||||
amt: ticketsPerPage,
|
url: "https://hub.pena.digital/heruvym/getTickets",
|
||||||
page: ticketApiPage,
|
ticketsPerPage,
|
||||||
status: "open",
|
ticketApiPage,
|
||||||
};
|
onNewTickets: useCallback(result => {
|
||||||
const controller = new AbortController();
|
if (result.data) updateTickets(result.data);
|
||||||
|
|
||||||
setTicketsFetchState("fetching");
|
|
||||||
getTickets({
|
|
||||||
body: getTicketsBody,
|
|
||||||
signal: controller.signal,
|
|
||||||
}).then(result => {
|
|
||||||
console.log("GetTicketsResponse", result);
|
|
||||||
if (result.data) {
|
|
||||||
updateTickets(result.data);
|
|
||||||
setTicketCount(result.count);
|
setTicketCount(result.count);
|
||||||
setTicketsFetchState("idle");
|
}, []),
|
||||||
} else setTicketsFetchState("all fetched");
|
onError: useCallback((error: Error) => {
|
||||||
}).catch(error => {
|
|
||||||
console.log("Error fetching tickets", error);
|
|
||||||
enqueueSnackbar(error.message);
|
enqueueSnackbar(error.message);
|
||||||
|
}, [])
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => controller.abort();
|
useSSESubscription<Ticket>({
|
||||||
}, [ticketApiPage, ticketsPerPage]);
|
enabled: Boolean(token),
|
||||||
|
url: `https://admin.pena.digital/heruvym/subscribe?Authorization=${token}`,
|
||||||
useEffect(function subscribeToTickets() {
|
onNewData: updateTickets,
|
||||||
if (!token) return;
|
onDisconnect: useCallback(() => {
|
||||||
|
|
||||||
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();
|
|
||||||
clearTickets();
|
clearTickets();
|
||||||
};
|
}, []),
|
||||||
}, [token]);
|
marker: "ticket"
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionWrapper
|
<SectionWrapper
|
||||||
@ -112,7 +83,7 @@ export default function Support() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CreateTicket />
|
<CreateTicket />
|
||||||
<TicketList />
|
<TicketList fetchState={fetchState} />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</SectionWrapper>
|
</SectionWrapper>
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
import { Box, Fab, FormControl, IconButton, InputAdornment, InputBase, Typography, useMediaQuery, useTheme, } from "@mui/material";
|
import { Box, Fab, FormControl, IconButton, InputAdornment, InputBase, Typography, useMediaQuery, useTheme, } from "@mui/material";
|
||||||
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
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 { useParams } from "react-router-dom";
|
||||||
import CustomButton from "@components/CustomButton";
|
import CustomButton from "@components/CustomButton";
|
||||||
import SendIcon from "@components/icons/SendIcon";
|
import SendIcon from "@components/icons/SendIcon";
|
||||||
import { throttle } from "@utils/decorators";
|
import { throttle } from "@frontend/kitui";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import { useTicketStore } from "@root/stores/tickets";
|
import { useTicketStore } from "@root/stores/tickets";
|
||||||
import { addOrUpdateMessages, clearMessageState, incrementMessageApiPage, setIsPreventAutoscroll, setMessageFetchState, useMessageStore } from "@root/stores/messages";
|
import { addOrUpdateMessages, clearMessageState, incrementMessageApiPage, setIsPreventAutoscroll, useMessageStore } from "@root/stores/messages";
|
||||||
import { getTicketMessages, sendTicketMessage, subscribeToTicketMessages } from "@root/api/tickets";
|
import { TicketMessage } from "@frontend/kitui";
|
||||||
import { GetMessagesRequest, TicketMessage } from "@root/model/ticket";
|
|
||||||
import { authStore } from "@root/stores/makeRequest";
|
|
||||||
import ChatMessage from "@root/components/ChatMessage";
|
import ChatMessage from "@root/components/ChatMessage";
|
||||||
|
import { useAuthStore } from "@root/stores/auth";
|
||||||
|
import { makeRequest } from "@root/api/makeRequest";
|
||||||
|
import { getMessageFromFetchError, useEventListener, useSSESubscription, useTicketMessages } from "@frontend/kitui";
|
||||||
|
|
||||||
|
|
||||||
export default function SupportChat() {
|
export default function SupportChat() {
|
||||||
@ -24,93 +25,53 @@ export default function SupportChat() {
|
|||||||
const lastMessageId = useMessageStore(state => state.lastMessageId);
|
const lastMessageId = useMessageStore(state => state.lastMessageId);
|
||||||
const messagesPerPage = useMessageStore(state => state.messagesPerPage);
|
const messagesPerPage = useMessageStore(state => state.messagesPerPage);
|
||||||
const isPreventAutoscroll = useMessageStore(state => state.isPreventAutoscroll);
|
const isPreventAutoscroll = useMessageStore(state => state.isPreventAutoscroll);
|
||||||
const messagesFetchStateRef = useRef(useMessageStore.getState().fetchState);
|
const token = useAuthStore(state => state.token);
|
||||||
const token = authStore(state => state.token);
|
|
||||||
const ticketId = useParams().ticketId;
|
const ticketId = useParams().ticketId;
|
||||||
const ticket = tickets.find(ticket => ticket.id === ticketId);
|
const ticket = tickets.find(ticket => ticket.id === ticketId);
|
||||||
const chatBoxRef = useRef<HTMLDivElement>();
|
const chatBoxRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(function fetchTicketMessages() {
|
const fetchState = useTicketMessages({
|
||||||
if (!ticketId) return;
|
makeRequest,
|
||||||
|
url: "https://admin.pena.digital/heruvym/getMessages",
|
||||||
const getTicketsBody: GetMessagesRequest = {
|
ticketId,
|
||||||
amt: messagesPerPage,
|
messagesPerPage,
|
||||||
page: messageApiPage,
|
messageApiPage,
|
||||||
ticket: ticketId,
|
onNewMessages: useCallback(messages => {
|
||||||
};
|
|
||||||
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;
|
if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) chatBoxRef.current.scrollTop = 1;
|
||||||
addOrUpdateMessages(result);
|
addOrUpdateMessages(messages);
|
||||||
setMessageFetchState("idle");
|
}, []),
|
||||||
} else setMessageFetchState("all fetched");
|
onError: useCallback((error: Error) => {
|
||||||
}).catch(error => {
|
|
||||||
console.log("Error fetching messages", error);
|
|
||||||
enqueueSnackbar(error.message);
|
enqueueSnackbar(error.message);
|
||||||
|
}, []),
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
useSSESubscription<TicketMessage>({
|
||||||
controller.abort();
|
enabled: Boolean(token) && Boolean(ticketId),
|
||||||
};
|
url: `https://admin.pena.digital/heruvym/ticket?ticket=${ticketId}&Authorization=${token}`,
|
||||||
}, [messageApiPage, messagesPerPage, ticketId]);
|
onNewData: addOrUpdateMessages,
|
||||||
|
onDisconnect: useCallback(() => {
|
||||||
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();
|
|
||||||
clearMessageState();
|
clearMessageState();
|
||||||
};
|
setIsPreventAutoscroll(false);
|
||||||
}, [ticketId, token]);
|
}, []),
|
||||||
|
marker: "ticket message"
|
||||||
useEffect(function attachScrollHandler() {
|
});
|
||||||
if (!chatBoxRef.current) return;
|
|
||||||
|
|
||||||
|
const throttledScrollHandler = useMemo(() => throttle(() => {
|
||||||
const chatBox = chatBoxRef.current;
|
const chatBox = chatBoxRef.current;
|
||||||
const scrollHandler = () => {
|
if (!chatBox) return;
|
||||||
|
|
||||||
const scrollBottom = chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight;
|
const scrollBottom = chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight;
|
||||||
const isPreventAutoscroll = scrollBottom > chatBox.clientHeight;
|
const isPreventAutoscroll = scrollBottom > chatBox.clientHeight * 20;
|
||||||
setIsPreventAutoscroll(isPreventAutoscroll);
|
setIsPreventAutoscroll(isPreventAutoscroll);
|
||||||
|
|
||||||
if (messagesFetchStateRef.current !== "idle") return;
|
if (fetchState !== "idle") return;
|
||||||
|
|
||||||
if (chatBox.scrollTop < chatBox.clientHeight) {
|
if (chatBox.scrollTop < chatBox.clientHeight) {
|
||||||
incrementMessageApiPage();
|
incrementMessageApiPage();
|
||||||
}
|
}
|
||||||
};
|
}, 200), [fetchState]);
|
||||||
|
|
||||||
const throttledScrollHandler = throttle(scrollHandler, 200);
|
useEventListener("scroll", throttledScrollHandler, chatBoxRef);
|
||||||
chatBox.addEventListener("scroll", throttledScrollHandler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
chatBox.removeEventListener("scroll", throttledScrollHandler);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(function scrollOnNewMessage() {
|
useEffect(function scrollOnNewMessage() {
|
||||||
if (!chatBoxRef.current) return;
|
if (!chatBoxRef.current) return;
|
||||||
@ -123,18 +84,25 @@ export default function SupportChat() {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [lastMessageId]);
|
}, [lastMessageId]);
|
||||||
|
|
||||||
useEffect(() => useMessageStore.subscribe(state => (messagesFetchStateRef.current = state.fetchState)), []);
|
|
||||||
|
|
||||||
function handleSendMessage() {
|
function handleSendMessage() {
|
||||||
if (!ticket || !messageField) return;
|
if (!ticket || !messageField) return;
|
||||||
|
|
||||||
sendTicketMessage({
|
makeRequest({
|
||||||
|
url: "https://hub.pena.digital/heruvym/send",
|
||||||
|
method: "POST",
|
||||||
|
useToken: true,
|
||||||
|
body: {
|
||||||
ticket: ticket.id,
|
ticket: ticket.id,
|
||||||
message: messageField,
|
message: messageField,
|
||||||
lang: "ru",
|
lang: "ru",
|
||||||
files: [],
|
files: [],
|
||||||
});
|
},
|
||||||
|
}).then(() => {
|
||||||
setMessageField("");
|
setMessageField("");
|
||||||
|
}).catch(error => {
|
||||||
|
const errorMessage = getMessageFromFetchError(error);
|
||||||
|
if (errorMessage) enqueueSnackbar(errorMessage);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToBottom(behavior?: ScrollBehavior) {
|
function scrollToBottom(behavior?: ScrollBehavior) {
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import { CircularProgress, List, ListItem, Box, useTheme, Pagination } from "@mui/material";
|
import { CircularProgress, List, ListItem, Box, useTheme, Pagination } from "@mui/material";
|
||||||
import TicketCard from "./TicketCard";
|
import TicketCard from "./TicketCard";
|
||||||
import { setTicketApiPage, useTicketStore } from "@root/stores/tickets";
|
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 theme = useTheme();
|
||||||
const tickets = useTicketStore(state => state.tickets);
|
const tickets = useTicketStore(state => state.tickets);
|
||||||
const ticketsFetchState = useTicketStore(state => state.fetchState);
|
|
||||||
const ticketCount = useTicketStore(state => state.ticketCount);
|
const ticketCount = useTicketStore(state => state.ticketCount);
|
||||||
const ticketApiPage = useTicketStore(state => state.apiPage);
|
const ticketApiPage = useTicketStore(state => state.apiPage);
|
||||||
const ticketsPerPage = useTicketStore(state => state.ticketsPerPage);
|
const ticketsPerPage = useTicketStore(state => state.ticketsPerPage);
|
||||||
@ -25,10 +28,11 @@ export default function TicketList() {
|
|||||||
<List
|
<List
|
||||||
sx={{
|
sx={{
|
||||||
p: 0,
|
p: 0,
|
||||||
|
minHeight: "120px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: "40px",
|
gap: "40px",
|
||||||
opacity: ticketsFetchState === "fetching" ? 0.4 : 1,
|
opacity: fetchState === "fetching" ? 0.4 : 1,
|
||||||
transitionProperty: "opacity",
|
transitionProperty: "opacity",
|
||||||
transitionDuration: "200ms",
|
transitionDuration: "200ms",
|
||||||
}}
|
}}
|
||||||
@ -43,13 +47,13 @@ export default function TicketList() {
|
|||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
{ticketsFetchState === "fetching" && (
|
{fetchState === "fetching" && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
minHeight: "80px",
|
minHeight: "120px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
|
@ -3,13 +3,13 @@ import SectionWrapper from "@components/SectionWrapper";
|
|||||||
import ComplexNavText from "@root/components/ComplexNavText";
|
import ComplexNavText from "@root/components/ComplexNavText";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import { setCustomTariffs, useCustomTariffsStore } from "@root/stores/customTariffs";
|
import { setCustomTariffs, useCustomTariffsStore } from "@root/stores/customTariffs";
|
||||||
import { useEffect } from "react";
|
import { useCallback } from "react";
|
||||||
import Summary from "./Summary";
|
import Summary from "./Summary";
|
||||||
import { fetchCustomTariffs } from "@root/api/customTariffs";
|
import { getMessageFromFetchError } from "@frontend/kitui";
|
||||||
import { getMessageFromFetchError } from "@root/utils/backendMessageHandler";
|
|
||||||
import ComplexHeader from "@root/components/ComplexHeader";
|
import ComplexHeader from "@root/components/ComplexHeader";
|
||||||
import CustomTariffCard from "./CustomTariffCard";
|
import CustomTariffCard from "./CustomTariffCard";
|
||||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||||
|
import { useCustomTariffs } from "@root/utils/hooks/useCustomTariffs";
|
||||||
|
|
||||||
|
|
||||||
export default function TariffConstructor() {
|
export default function TariffConstructor() {
|
||||||
@ -17,16 +17,15 @@ export default function TariffConstructor() {
|
|||||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
const customTariffs = useCustomTariffsStore(state => state.customTariffs);
|
const customTariffs = useCustomTariffsStore(state => state.customTariffs);
|
||||||
|
|
||||||
useEffect(function getCustomTariffs() {
|
useCustomTariffs({
|
||||||
const controller = new AbortController();
|
url: "https://admin.pena.digital/strator/privilege/service",
|
||||||
fetchCustomTariffs(controller.signal).then(setCustomTariffs).catch(error => {
|
onNewUser: setCustomTariffs,
|
||||||
|
onError: useCallback(error => {
|
||||||
const errorMessage = getMessageFromFetchError(error, "Не удалось получить кастомные тарифы");
|
const errorMessage = getMessageFromFetchError(error, "Не удалось получить кастомные тарифы");
|
||||||
if (errorMessage) enqueueSnackbar(errorMessage);
|
if (errorMessage) enqueueSnackbar(errorMessage);
|
||||||
|
}, [])
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => controller.abort();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionWrapper
|
<SectionWrapper
|
||||||
maxWidth="lg"
|
maxWidth="lg"
|
||||||
|
@ -2,7 +2,6 @@ import { Box, Dialog, IconButton, Link, Typography, useMediaQuery, useTheme } fr
|
|||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { authStore } from "@stores/makeRequest";
|
|
||||||
import CustomButton from "@components/CustomButton";
|
import CustomButton from "@components/CustomButton";
|
||||||
import InputTextfield from "@components/InputTextfield";
|
import InputTextfield from "@components/InputTextfield";
|
||||||
import PenaLogo from "@components/PenaLogo";
|
import PenaLogo from "@components/PenaLogo";
|
||||||
@ -12,7 +11,8 @@ import { object, string } from "yup";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { LoginRequest, LoginResponse } from "@root/model/auth";
|
import { LoginRequest, LoginResponse } from "@root/model/auth";
|
||||||
import { setUserId, useUserStore } from "@root/stores/user";
|
import { setUserId, useUserStore } from "@root/stores/user";
|
||||||
import { getMessageFromFetchError } from "@root/utils/backendMessageHandler";
|
import { getMessageFromFetchError } from "@frontend/kitui";
|
||||||
|
import { makeRequest } from "@root/api/makeRequest";
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
login: string;
|
login: string;
|
||||||
@ -35,7 +35,6 @@ export default function SigninDialog() {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const makeRequest = authStore((state) => state.makeRequest);
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const formik = useFormik<Values>({
|
const formik = useFormik<Values>({
|
||||||
initialValues,
|
initialValues,
|
||||||
|
@ -6,13 +6,13 @@ import CustomButton from "@components/CustomButton";
|
|||||||
import InputTextfield from "@components/InputTextfield";
|
import InputTextfield from "@components/InputTextfield";
|
||||||
import PenaLogo from "@components/PenaLogo";
|
import PenaLogo from "@components/PenaLogo";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import { authStore } from "@stores/makeRequest";
|
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { object, ref, string } from "yup";
|
import { object, ref, string } from "yup";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { RegisterRequest, RegisterResponse } from "@root/model/auth";
|
import { RegisterRequest, RegisterResponse } from "@root/model/auth";
|
||||||
import { setUserId, useUserStore } from "@root/stores/user";
|
import { setUserId, useUserStore } from "@root/stores/user";
|
||||||
import { getMessageFromFetchError } from "@root/utils/backendMessageHandler";
|
import { getMessageFromFetchError } from "@frontend/kitui";
|
||||||
|
import { makeRequest } from "@root/api/makeRequest";
|
||||||
|
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
@ -39,7 +39,6 @@ export default function SignupDialog() {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const makeRequest = authStore(state => state.makeRequest);
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const formik = useFormik<Values>({
|
const formik = useFormik<Values>({
|
||||||
initialValues,
|
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 { create } from "zustand";
|
||||||
import { devtools } from "zustand/middleware";
|
import { devtools } from "zustand/middleware";
|
||||||
|
|
||||||
|
|
||||||
interface MessageStore {
|
interface MessageStore {
|
||||||
messages: TicketMessage[];
|
messages: TicketMessage[];
|
||||||
fetchState: "idle" | "fetching" | "all fetched";
|
|
||||||
apiPage: number;
|
apiPage: number;
|
||||||
messagesPerPage: number;
|
messagesPerPage: number;
|
||||||
lastMessageId: string | undefined;
|
lastMessageId: string | undefined;
|
||||||
@ -17,7 +16,6 @@ export const useMessageStore = create<MessageStore>()(
|
|||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
messages: [],
|
messages: [],
|
||||||
messagesPerPage: 10,
|
messagesPerPage: 10,
|
||||||
fetchState: "idle",
|
|
||||||
apiPage: 0,
|
apiPage: 0,
|
||||||
lastMessageId: undefined,
|
lastMessageId: undefined,
|
||||||
isPreventAutoscroll: false,
|
isPreventAutoscroll: false,
|
||||||
@ -46,11 +44,8 @@ export const clearMessageState = () => useMessageStore.setState({
|
|||||||
messages: [],
|
messages: [],
|
||||||
apiPage: 0,
|
apiPage: 0,
|
||||||
lastMessageId: undefined,
|
lastMessageId: undefined,
|
||||||
fetchState: "idle",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setMessageFetchState = (fetchState: MessageStore["fetchState"]) => useMessageStore.setState({ fetchState });
|
|
||||||
|
|
||||||
export const incrementMessageApiPage = () => {
|
export const incrementMessageApiPage = () => {
|
||||||
const state = useMessageStore.getState();
|
const state = useMessageStore.getState();
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Ticket } from "@root/model/ticket";
|
import { Ticket } from "@frontend/kitui";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { devtools } from "zustand/middleware";
|
import { devtools } from "zustand/middleware";
|
||||||
|
|
||||||
@ -6,7 +6,6 @@ import { devtools } from "zustand/middleware";
|
|||||||
interface TicketStore {
|
interface TicketStore {
|
||||||
ticketCount: number;
|
ticketCount: number;
|
||||||
tickets: Ticket[];
|
tickets: Ticket[];
|
||||||
fetchState: "idle" | "fetching" | "all fetched";
|
|
||||||
apiPage: number;
|
apiPage: number;
|
||||||
ticketsPerPage: number;
|
ticketsPerPage: number;
|
||||||
}
|
}
|
||||||
@ -28,8 +27,6 @@ export const useTicketStore = create<TicketStore>()(
|
|||||||
|
|
||||||
export const setTicketCount = (ticketCount: number) => useTicketStore.setState({ ticketCount });
|
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 setTicketApiPage = (apiPage: number) => useTicketStore.setState({ apiPage: apiPage });
|
||||||
|
|
||||||
export const updateTickets = (receivedTickets: Ticket[]) => {
|
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 { create } from "zustand";
|
||||||
import { createJSONStorage, devtools, persist } from "zustand/middleware";
|
import { createJSONStorage, devtools, persist } from "zustand/middleware";
|
||||||
|
|
||||||
@ -10,7 +10,6 @@ interface UnauthTicketStore {
|
|||||||
} | null;
|
} | null;
|
||||||
isMessageSending: boolean;
|
isMessageSending: boolean;
|
||||||
messages: TicketMessage[];
|
messages: TicketMessage[];
|
||||||
fetchState: "idle" | "fetching" | "all fetched";
|
|
||||||
apiPage: number;
|
apiPage: number;
|
||||||
messagesPerPage: number;
|
messagesPerPage: number;
|
||||||
lastMessageId: string | undefined;
|
lastMessageId: string | undefined;
|
||||||
@ -24,7 +23,6 @@ export const useUnauthTicketStore = create<UnauthTicketStore>()(
|
|||||||
sessionData: null,
|
sessionData: null,
|
||||||
isMessageSending: false,
|
isMessageSending: false,
|
||||||
messages: [],
|
messages: [],
|
||||||
fetchState: "idle",
|
|
||||||
apiPage: 0,
|
apiPage: 0,
|
||||||
messagesPerPage: 10,
|
messagesPerPage: 10,
|
||||||
lastMessageId: undefined,
|
lastMessageId: undefined,
|
||||||
@ -62,8 +60,6 @@ export const addOrUpdateUnauthMessages = (receivedMessages: TicketMessage[]) =>
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setUnauthTicketFetchState = (fetchState: UnauthTicketStore["fetchState"]) => useUnauthTicketStore.setState({ fetchState });
|
|
||||||
|
|
||||||
export const incrementUnauthMessageApiPage = () => {
|
export const incrementUnauthMessageApiPage = () => {
|
||||||
const state = useUnauthTicketStore.getState();
|
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;
|
|
||||||
}
|
|
34
src/utils/hooks/useUser.ts
Normal file
34
src/utils/hooks/useUser.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { createMakeRequest, devlog } from "@frontend/kitui";
|
||||||
|
import { User } from "@root/model/user";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
export function useUser({ makeRequest, onError, onNewUser, url, userId }: {
|
||||||
|
makeRequest: ReturnType<typeof createMakeRequest>;
|
||||||
|
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();
|
||||||
|
}, [makeRequest, onError, onNewUser, url, userId]);
|
||||||
|
}
|
@ -1,12 +1,12 @@
|
|||||||
|
import { useAuthStore } from "@root/stores/auth";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useLocation, Navigate } from "react-router-dom";
|
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) => {
|
export default ({ children }: Props) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { token } = authStore();
|
const token = useAuthStore(state => state.token);
|
||||||
console.log(token);
|
console.log(token);
|
||||||
//Если пользователь авторизован, перенаправляем его в приложение. Иначе пускаем куда хотел
|
//Если пользователь авторизован, перенаправляем его в приложение. Иначе пускаем куда хотел
|
||||||
if (token) {
|
if (token) {
|
||||||
|
33
yarn.lock
33
yarn.lock
@ -1450,6 +1450,14 @@
|
|||||||
minimatch "^3.1.2"
|
minimatch "^3.1.2"
|
||||||
strip-json-comments "^3.1.1"
|
strip-json-comments "^3.1.1"
|
||||||
|
|
||||||
|
"@frontend/kitui@^1.0.2":
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.4.tgz#32c842f5aeb6d71d2735cc8f3cad4f902c601dd9"
|
||||||
|
integrity sha1-MshC9a621x0nNcyPPK1PkCxgHdk=
|
||||||
|
dependencies:
|
||||||
|
axios "^1.4.0"
|
||||||
|
reconnecting-eventsource "^1.6.2"
|
||||||
|
|
||||||
"@humanwhocodes/config-array@^0.11.6":
|
"@humanwhocodes/config-array@^0.11.6":
|
||||||
version "0.11.7"
|
version "0.11.7"
|
||||||
resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz"
|
resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz"
|
||||||
@ -3234,7 +3242,7 @@ async@^3.2.3:
|
|||||||
|
|
||||||
asynckit@^0.4.0:
|
asynckit@^0.4.0:
|
||||||
version "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==
|
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||||
|
|
||||||
at-least-node@^1.0.0:
|
at-least-node@^1.0.0:
|
||||||
@ -3273,6 +3281,15 @@ axios@^1.3.4:
|
|||||||
form-data "^4.0.0"
|
form-data "^4.0.0"
|
||||||
proxy-from-env "^1.1.0"
|
proxy-from-env "^1.1.0"
|
||||||
|
|
||||||
|
axios@^1.4.0:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f"
|
||||||
|
integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==
|
||||||
|
dependencies:
|
||||||
|
follow-redirects "^1.15.0"
|
||||||
|
form-data "^4.0.0"
|
||||||
|
proxy-from-env "^1.1.0"
|
||||||
|
|
||||||
axobject-query@^2.2.0:
|
axobject-query@^2.2.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz"
|
resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz"
|
||||||
@ -3797,7 +3814,7 @@ colorette@^2.0.10:
|
|||||||
|
|
||||||
combined-stream@^1.0.8:
|
combined-stream@^1.0.8:
|
||||||
version "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==
|
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream "~1.0.0"
|
delayed-stream "~1.0.0"
|
||||||
@ -4278,7 +4295,7 @@ defined@^1.0.0:
|
|||||||
|
|
||||||
delayed-stream@~1.0.0:
|
delayed-stream@~1.0.0:
|
||||||
version "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==
|
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||||
|
|
||||||
depd@2.0.0:
|
depd@2.0.0:
|
||||||
@ -5180,7 +5197,7 @@ flatted@^3.1.0:
|
|||||||
|
|
||||||
follow-redirects@^1.0.0, follow-redirects@^1.15.0:
|
follow-redirects@^1.0.0, follow-redirects@^1.15.0:
|
||||||
version "1.15.2"
|
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==
|
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
|
||||||
|
|
||||||
for-each@^0.3.3:
|
for-each@^0.3.3:
|
||||||
@ -5220,7 +5237,7 @@ form-data@^3.0.0:
|
|||||||
|
|
||||||
form-data@^4.0.0:
|
form-data@^4.0.0:
|
||||||
version "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==
|
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
|
||||||
dependencies:
|
dependencies:
|
||||||
asynckit "^0.4.0"
|
asynckit "^0.4.0"
|
||||||
@ -7341,12 +7358,12 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5:
|
|||||||
|
|
||||||
mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
|
mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
|
||||||
version "1.52.0"
|
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==
|
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:
|
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"
|
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==
|
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-db "1.52.0"
|
mime-db "1.52.0"
|
||||||
@ -8504,7 +8521,7 @@ proxy-addr@~2.0.7:
|
|||||||
|
|
||||||
proxy-from-env@^1.1.0:
|
proxy-from-env@^1.1.0:
|
||||||
version "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==
|
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||||
|
|
||||||
psl@^1.1.33:
|
psl@^1.1.33:
|
||||||
|
Loading…
Reference in New Issue
Block a user