add stuff
This commit is contained in:
parent
6ad843a2a0
commit
a9ca5294dd
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
22
package.json
22
package.json
@ -1,12 +1,32 @@
|
|||||||
{
|
{
|
||||||
"name": "@frontend/kitui",
|
"name": "@frontend/kitui",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "test",
|
"description": "test",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
"types": "index.d.ts",
|
||||||
"repository": "git@penahub.gitlab.yandexcloud.net:frontend/kitui.git",
|
"repository": "git@penahub.gitlab.yandexcloud.net:frontend/kitui.git",
|
||||||
"author": "skeris <kotilion.95@gmail.com>",
|
"author": "skeris <kotilion.95@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"clean": "rm -rf dist",
|
||||||
|
"build": "npm run clean && tsc && cp package.json README.md ./dist"
|
||||||
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"registry": "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/"
|
"registry": "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.2.5",
|
||||||
|
"@types/react": "^18.2.7",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"typescript": "^5.0.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.4.0",
|
||||||
|
"reconnecting-eventsource": "^1.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
72
src/api/createMakeRequest.ts
Normal file
72
src/api/createMakeRequest.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import axios, { AxiosResponse, Method } from "axios";
|
||||||
|
|
||||||
|
|
||||||
|
interface MakeRequestArgs<T> {
|
||||||
|
method?: Method;
|
||||||
|
url: string;
|
||||||
|
body?: T;
|
||||||
|
/** Send access token */
|
||||||
|
useToken?: boolean;
|
||||||
|
contentType?: boolean;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
/** Send refresh token */
|
||||||
|
withCredentials?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createMakeRequest = (getToken: () => string, setToken: (token: string) => void) => async <TRequest = unknown, TResponse = unknown>({
|
||||||
|
method = "post",
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
useToken = true,
|
||||||
|
contentType = false,
|
||||||
|
signal,
|
||||||
|
withCredentials,
|
||||||
|
}: MakeRequestArgs<TRequest>): Promise<TResponse> => {
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
if (useToken) headers["Authorization"] = `Bearer ${getToken()}`;
|
||||||
|
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) {
|
||||||
|
setToken(response.data.accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 401 && !withCredentials) {
|
||||||
|
const refreshResponse = await refresh(getToken());
|
||||||
|
if (refreshResponse.data?.accessToken) setToken(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: string) {
|
||||||
|
return axios<never, AxiosResponse<{ accessToken: string; }>>("https://admin.pena.digital/auth/refresh", {
|
||||||
|
headers: {
|
||||||
|
"Authorization": token,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
2
src/api/index.ts
Normal file
2
src/api/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./createMakeRequest";
|
||||||
|
export * from "./tickets";
|
18
src/api/tickets.ts
Normal file
18
src/api/tickets.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { CreateTicketRequest, CreateTicketResponse } from "../model/ticket";
|
||||||
|
import { createMakeRequest } from "./createMakeRequest";
|
||||||
|
|
||||||
|
|
||||||
|
export function createTicket({ makeRequest, url,body, useToken = true }: {
|
||||||
|
makeRequest: ReturnType<typeof createMakeRequest>;
|
||||||
|
url: string;
|
||||||
|
body: CreateTicketRequest;
|
||||||
|
useToken?: boolean;
|
||||||
|
}): Promise<CreateTicketResponse> {
|
||||||
|
return makeRequest({
|
||||||
|
url,
|
||||||
|
method: "POST",
|
||||||
|
useToken,
|
||||||
|
body,
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
}
|
1
src/decorators/index.ts
Normal file
1
src/decorators/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./throttle";
|
29
src/decorators/throttle.ts
Normal file
29
src/decorators/throttle.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
export 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;
|
||||||
|
}
|
6
src/hooks/index.ts
Normal file
6
src/hooks/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * from "./useDebounce";
|
||||||
|
export * from "./useEventListener";
|
||||||
|
export * from "./useSSESubscription";
|
||||||
|
export * from "./useThrottle";
|
||||||
|
export * from "./useTicketMessages";
|
||||||
|
export * from "./useTickets";
|
15
src/hooks/useDebounce.ts
Normal file
15
src/hooks/useDebounce.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
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;
|
||||||
|
}
|
82
src/hooks/useEventListener.ts
Normal file
82
src/hooks/useEventListener.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { useEffect, useRef, RefObject, useLayoutEffect } from "react";
|
||||||
|
|
||||||
|
// https://usehooks-ts.com/react-hook/use-event-listener
|
||||||
|
|
||||||
|
// MediaQueryList Event based useEventListener interface
|
||||||
|
function useEventListener<K extends keyof MediaQueryListEventMap>(
|
||||||
|
eventName: K,
|
||||||
|
handler: (event: MediaQueryListEventMap[K]) => void,
|
||||||
|
element: RefObject<MediaQueryList>,
|
||||||
|
options?: boolean | AddEventListenerOptions,
|
||||||
|
): void;
|
||||||
|
|
||||||
|
// Window Event based useEventListener interface
|
||||||
|
function useEventListener<K extends keyof WindowEventMap>(
|
||||||
|
eventName: K,
|
||||||
|
handler: (event: WindowEventMap[K]) => void,
|
||||||
|
element?: undefined,
|
||||||
|
options?: boolean | AddEventListenerOptions,
|
||||||
|
): void;
|
||||||
|
|
||||||
|
// Element Event based useEventListener interface
|
||||||
|
function useEventListener<
|
||||||
|
K extends keyof HTMLElementEventMap,
|
||||||
|
T extends HTMLElement = HTMLDivElement,
|
||||||
|
>(
|
||||||
|
eventName: K,
|
||||||
|
handler: (event: HTMLElementEventMap[K]) => void,
|
||||||
|
element: RefObject<T>,
|
||||||
|
options?: boolean | AddEventListenerOptions,
|
||||||
|
): void;
|
||||||
|
|
||||||
|
// Document Event based useEventListener interface
|
||||||
|
function useEventListener<K extends keyof DocumentEventMap>(
|
||||||
|
eventName: K,
|
||||||
|
handler: (event: DocumentEventMap[K]) => void,
|
||||||
|
element: RefObject<Document>,
|
||||||
|
options?: boolean | AddEventListenerOptions,
|
||||||
|
): void;
|
||||||
|
|
||||||
|
function useEventListener<
|
||||||
|
KW extends keyof WindowEventMap,
|
||||||
|
KH extends keyof HTMLElementEventMap,
|
||||||
|
KM extends keyof MediaQueryListEventMap,
|
||||||
|
T extends HTMLElement | MediaQueryList | void = void,
|
||||||
|
>(
|
||||||
|
eventName: KW | KH | KM,
|
||||||
|
handler: (
|
||||||
|
event:
|
||||||
|
| WindowEventMap[KW]
|
||||||
|
| HTMLElementEventMap[KH]
|
||||||
|
| MediaQueryListEventMap[KM]
|
||||||
|
| Event,
|
||||||
|
) => void,
|
||||||
|
element?: RefObject<T>,
|
||||||
|
options?: boolean | AddEventListenerOptions,
|
||||||
|
) {
|
||||||
|
// Create a ref that stores handler
|
||||||
|
const savedHandler = useRef(handler);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
savedHandler.current = handler;
|
||||||
|
}, [handler]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Define the listening target
|
||||||
|
const targetElement: T | Window = element?.current ?? window;
|
||||||
|
|
||||||
|
if (!(targetElement && targetElement.addEventListener)) return;
|
||||||
|
|
||||||
|
// Create event listener that calls handler function stored in ref
|
||||||
|
const listener: typeof handler = event => savedHandler.current(event);
|
||||||
|
|
||||||
|
targetElement.addEventListener(eventName, listener, options);
|
||||||
|
|
||||||
|
// Remove event listener on cleanup
|
||||||
|
return () => {
|
||||||
|
targetElement.removeEventListener(eventName, listener, options);
|
||||||
|
};
|
||||||
|
}, [eventName, element, options]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useEventListener;
|
38
src/hooks/useSSESubscription.ts
Normal file
38
src/hooks/useSSESubscription.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import ReconnectingEventSource from "reconnecting-eventsource";
|
||||||
|
import { devlog } from "../utils";
|
||||||
|
|
||||||
|
|
||||||
|
export function useSSESubscription<T>({ enabled = true, url, onNewData, onDisconnect, marker = "" }: {
|
||||||
|
enabled?: boolean;
|
||||||
|
url: string;
|
||||||
|
onNewData: (data: T[]) => void;
|
||||||
|
onDisconnect: () => void;
|
||||||
|
marker?: string;
|
||||||
|
}) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled) return;
|
||||||
|
|
||||||
|
const eventSource = new ReconnectingEventSource(url);
|
||||||
|
|
||||||
|
eventSource.addEventListener("open", () => devlog(`EventSource connected with ${url}`));
|
||||||
|
eventSource.addEventListener("close", () => devlog(`EventSource closed with ${url}`));
|
||||||
|
eventSource.addEventListener("message", event => {
|
||||||
|
try {
|
||||||
|
const newData = JSON.parse(event.data) as T;
|
||||||
|
devlog(`new SSE: ${marker}`, newData);
|
||||||
|
onNewData([newData]);
|
||||||
|
} catch (error) {
|
||||||
|
devlog(`SSE parsing error: ${marker}`, event.data, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
eventSource.addEventListener("error", event => {
|
||||||
|
devlog("SSE Error:", event);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
eventSource.close();
|
||||||
|
onDisconnect();
|
||||||
|
};
|
||||||
|
}, [enabled, marker, onDisconnect, onNewData, url]);
|
||||||
|
}
|
22
src/hooks/useThrottle.ts
Normal file
22
src/hooks/useThrottle.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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;
|
||||||
|
}
|
51
src/hooks/useTicketMessages.ts
Normal file
51
src/hooks/useTicketMessages.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { createMakeRequest } from "../api";
|
||||||
|
import { TicketMessage, GetMessagesRequest, GetMessagesResponse } from "../model";
|
||||||
|
import { devlog } from "../utils";
|
||||||
|
|
||||||
|
|
||||||
|
export function useTicketMessages({ makeRequest, url, messageApiPage, messagesPerPage, ticketId, onNewMessages, onError, isUnauth = false }: {
|
||||||
|
makeRequest: ReturnType<typeof createMakeRequest>;
|
||||||
|
url: string;
|
||||||
|
ticketId: string | undefined;
|
||||||
|
messagesPerPage: number;
|
||||||
|
messageApiPage: number;
|
||||||
|
onNewMessages: (messages: TicketMessage[]) => void;
|
||||||
|
onError: (error: Error) => void;
|
||||||
|
isUnauth?: boolean;
|
||||||
|
}) {
|
||||||
|
const [fetchState, setFetchState] = useState<"fetching" | "idle" | "all fetched">("idle");
|
||||||
|
|
||||||
|
useEffect(function fetchTicketMessages() {
|
||||||
|
if (!ticketId) return;
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
setFetchState("fetching");
|
||||||
|
makeRequest<GetMessagesRequest, GetMessagesResponse>({
|
||||||
|
url,
|
||||||
|
method: "POST",
|
||||||
|
useToken: !isUnauth,
|
||||||
|
body: {
|
||||||
|
amt: messagesPerPage,
|
||||||
|
page: messageApiPage,
|
||||||
|
ticket: ticketId,
|
||||||
|
},
|
||||||
|
signal: controller.signal,
|
||||||
|
withCredentials: isUnauth,
|
||||||
|
}).then(result => {
|
||||||
|
devlog("GetMessagesResponse", result);
|
||||||
|
if (result?.length > 0) {
|
||||||
|
onNewMessages(result);
|
||||||
|
setFetchState("idle");
|
||||||
|
} else setFetchState("all fetched");
|
||||||
|
}).catch(error => {
|
||||||
|
devlog("Error fetching messages", error);
|
||||||
|
onError(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => controller.abort();
|
||||||
|
}, [isUnauth, makeRequest, messageApiPage, messagesPerPage, onError, onNewMessages, ticketId, url]);
|
||||||
|
|
||||||
|
return fetchState;
|
||||||
|
}
|
46
src/hooks/useTickets.ts
Normal file
46
src/hooks/useTickets.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { createMakeRequest } from "../api";
|
||||||
|
import { GetTicketsResponse, GetTicketsRequest } from "../model";
|
||||||
|
import { devlog } from "../utils";
|
||||||
|
|
||||||
|
|
||||||
|
export function useTickets({ makeRequest, url, ticketsPerPage, ticketApiPage, onNewTickets, onError }: {
|
||||||
|
makeRequest: ReturnType<typeof createMakeRequest>;
|
||||||
|
url: string;
|
||||||
|
ticketsPerPage: number;
|
||||||
|
ticketApiPage: number;
|
||||||
|
onNewTickets: (response: GetTicketsResponse) => void;
|
||||||
|
onError: (error: Error) => void;
|
||||||
|
}) {
|
||||||
|
const [fetchState, setFetchState] = useState<"fetching" | "idle" | "all fetched">("idle");
|
||||||
|
|
||||||
|
useEffect(function fetchTickets() {
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
setFetchState("fetching");
|
||||||
|
makeRequest<GetTicketsRequest, GetTicketsResponse>({
|
||||||
|
url,
|
||||||
|
method: "POST",
|
||||||
|
useToken: true,
|
||||||
|
body: {
|
||||||
|
amt: ticketsPerPage,
|
||||||
|
page: ticketApiPage,
|
||||||
|
status: "open",
|
||||||
|
},
|
||||||
|
signal: controller.signal,
|
||||||
|
}).then((result) => {
|
||||||
|
devlog("GetTicketsResponse", result);
|
||||||
|
if (result.data) {
|
||||||
|
onNewTickets(result);
|
||||||
|
setFetchState("idle");
|
||||||
|
} else setFetchState("all fetched");
|
||||||
|
}).catch(error => {
|
||||||
|
console.log("Error fetching tickets", error);
|
||||||
|
onError(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => controller.abort();
|
||||||
|
}, [makeRequest, onError, onNewTickets, ticketApiPage, ticketsPerPage, url]);
|
||||||
|
|
||||||
|
return fetchState;
|
||||||
|
}
|
6
src/index.ts
Normal file
6
src/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * from "./api";
|
||||||
|
export * from "./hooks";
|
||||||
|
export * from "./decorators";
|
||||||
|
export * from "./utils";
|
||||||
|
|
||||||
|
export type * from "./model";
|
1
src/model/index.ts
Normal file
1
src/model/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type * from "./ticket";
|
67
src/model/ticket.ts
Normal file
67
src/model/ticket.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
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[];
|
29
src/utils/backendMessageHandler.ts
Normal file
29
src/utils/backendMessageHandler.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { isAxiosError } from "axios";
|
||||||
|
import { devlog } from "./devlog";
|
||||||
|
|
||||||
|
|
||||||
|
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 {
|
||||||
|
devlog(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 ?? "Что-то пошло не так. Повторите попытку позже";
|
||||||
|
}
|
3
src/utils/devlog.ts
Normal file
3
src/utils/devlog.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const devlog: typeof console.log = (...args) => {
|
||||||
|
if (process.env.NODE_ENV === "develpment") console.log(...args);
|
||||||
|
};
|
2
src/utils/index.ts
Normal file
2
src/utils/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./devlog";
|
||||||
|
export * from "./backendMessageHandler";
|
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"declaration": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"target": "es6",
|
||||||
|
"module": "es6",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
133
yarn.lock
Normal file
133
yarn.lock
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@types/node@^20.2.5":
|
||||||
|
version "20.2.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.2.5.tgz#26d295f3570323b2837d322180dfbf1ba156fefb"
|
||||||
|
integrity sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==
|
||||||
|
|
||||||
|
"@types/prop-types@*":
|
||||||
|
version "15.7.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
||||||
|
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
|
||||||
|
|
||||||
|
"@types/react@^18.2.7":
|
||||||
|
version "18.2.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.7.tgz#dfb4518042a3117a045b8c222316f83414a783b3"
|
||||||
|
integrity sha512-ojrXpSH2XFCmHm7Jy3q44nXDyN54+EYKP2lBhJ2bqfyPj6cIUW/FZW/Csdia34NQgq7KYcAlHi5184m4X88+yw==
|
||||||
|
dependencies:
|
||||||
|
"@types/prop-types" "*"
|
||||||
|
"@types/scheduler" "*"
|
||||||
|
csstype "^3.0.2"
|
||||||
|
|
||||||
|
"@types/scheduler@*":
|
||||||
|
version "0.16.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
|
||||||
|
integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==
|
||||||
|
|
||||||
|
asynckit@^0.4.0:
|
||||||
|
version "0.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||||
|
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
combined-stream@^1.0.8:
|
||||||
|
version "1.0.8"
|
||||||
|
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"
|
||||||
|
|
||||||
|
csstype@^3.0.2:
|
||||||
|
version "3.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
||||||
|
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
|
||||||
|
|
||||||
|
delayed-stream@~1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||||
|
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||||
|
|
||||||
|
follow-redirects@^1.15.0:
|
||||||
|
version "1.15.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
|
||||||
|
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
|
||||||
|
|
||||||
|
form-data@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
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"
|
||||||
|
combined-stream "^1.0.8"
|
||||||
|
mime-types "^2.1.12"
|
||||||
|
|
||||||
|
"js-tokens@^3.0.0 || ^4.0.0":
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||||
|
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||||
|
|
||||||
|
loose-envify@^1.1.0:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||||
|
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||||
|
dependencies:
|
||||||
|
js-tokens "^3.0.0 || ^4.0.0"
|
||||||
|
|
||||||
|
mime-db@1.52.0:
|
||||||
|
version "1.52.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||||
|
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||||
|
|
||||||
|
mime-types@^2.1.12:
|
||||||
|
version "2.1.35"
|
||||||
|
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"
|
||||||
|
|
||||||
|
proxy-from-env@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
||||||
|
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||||
|
|
||||||
|
react-dom@^18.2.0:
|
||||||
|
version "18.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
||||||
|
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
scheduler "^0.23.0"
|
||||||
|
|
||||||
|
react@^18.2.0:
|
||||||
|
version "18.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||||
|
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
|
||||||
|
reconnecting-eventsource@^1.6.2:
|
||||||
|
version "1.6.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/reconnecting-eventsource/-/reconnecting-eventsource-1.6.2.tgz#b7f5b03b1c76291f6fbcb0203004892a57ae253b"
|
||||||
|
integrity sha512-vHhoxVLbA2YcfljWMKEbgR1KVTgwIrnyh/bzVJc+gfQbGcUIToLL6jNhkUL4E+9FbnAcfUVNLIw2YCiliTg/4g==
|
||||||
|
|
||||||
|
scheduler@^0.23.0:
|
||||||
|
version "0.23.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
|
||||||
|
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
|
||||||
|
typescript@^5.0.4:
|
||||||
|
version "5.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b"
|
||||||
|
integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==
|
Loading…
Reference in New Issue
Block a user