diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..54df0b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist + +npm-debug.log* +yarn-debug.log* +yarn-error.log* \ No newline at end of file diff --git a/package.json b/package.json index 49f4b6b..a00ec43 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,32 @@ { - "name": "@frontend/kitui", - "version": "1.0.0", - "description": "test", - "main": "index.js", - "repository": "git@penahub.gitlab.yandexcloud.net:frontend/kitui.git", - "author": "skeris ", - "license": "MIT", - "publishConfig": { - "registry": "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/" - } + "name": "@frontend/kitui", + "version": "1.0.1", + "description": "test", + "main": "index.js", + "types": "index.d.ts", + "repository": "git@penahub.gitlab.yandexcloud.net:frontend/kitui.git", + "author": "skeris ", + "license": "MIT", + "scripts": { + "clean": "rm -rf dist", + "build": "npm run clean && tsc && cp package.json README.md ./dist" + }, + "publishConfig": { + "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" + } } diff --git a/src/api/createMakeRequest.ts b/src/api/createMakeRequest.ts new file mode 100644 index 0000000..0b1a08f --- /dev/null +++ b/src/api/createMakeRequest.ts @@ -0,0 +1,72 @@ +import axios, { AxiosResponse, Method } from "axios"; + + +interface MakeRequestArgs { + 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 ({ + method = "post", + url, + body, + useToken = true, + contentType = false, + signal, + withCredentials, +}: MakeRequestArgs): Promise => { + const headers: Record = {}; + if (useToken) headers["Authorization"] = `Bearer ${getToken()}`; + if (contentType) headers["Content-Type"] = "application/json"; + + try { + const response = await axios>({ + 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>({ + url, + method, + headers, + data: body, + signal, + }); + + return response.data; + } + + throw error; + } +}; + +function refresh(token: string) { + return axios>("https://admin.pena.digital/auth/refresh", { + headers: { + "Authorization": token, + "Content-Type": "application/json", + }, + }); +} \ No newline at end of file diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..e322152 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,2 @@ +export * from "./createMakeRequest"; +export * from "./tickets"; \ No newline at end of file diff --git a/src/api/tickets.ts b/src/api/tickets.ts new file mode 100644 index 0000000..dee3171 --- /dev/null +++ b/src/api/tickets.ts @@ -0,0 +1,18 @@ +import { CreateTicketRequest, CreateTicketResponse } from "../model/ticket"; +import { createMakeRequest } from "./createMakeRequest"; + + +export function createTicket({ makeRequest, url,body, useToken = true }: { + makeRequest: ReturnType; + url: string; + body: CreateTicketRequest; + useToken?: boolean; +}): Promise { + return makeRequest({ + url, + method: "POST", + useToken, + body, + withCredentials: true, + }); +} \ No newline at end of file diff --git a/src/decorators/index.ts b/src/decorators/index.ts new file mode 100644 index 0000000..b80e71d --- /dev/null +++ b/src/decorators/index.ts @@ -0,0 +1 @@ +export * from "./throttle"; \ No newline at end of file diff --git a/src/decorators/throttle.ts b/src/decorators/throttle.ts new file mode 100644 index 0000000..3d7a07e --- /dev/null +++ b/src/decorators/throttle.ts @@ -0,0 +1,29 @@ +export type ThrottledFunction any> = (...args: Parameters) => void; + +export function throttle any>(func: T, ms: number): ThrottledFunction { + let isThrottled = false; + let savedArgs: Parameters | null; + let savedThis: any; + + function wrapper(this: any, ...args: Parameters) { + 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; +} \ No newline at end of file diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..10da578 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,6 @@ +export * from "./useDebounce"; +export * from "./useEventListener"; +export * from "./useSSESubscription"; +export * from "./useThrottle"; +export * from "./useTicketMessages"; +export * from "./useTickets"; \ No newline at end of file diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts new file mode 100644 index 0000000..341598c --- /dev/null +++ b/src/hooks/useDebounce.ts @@ -0,0 +1,15 @@ +import { useState, useEffect } from "react"; + + +export function useDebounce(value: T, delay: number) { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + return () => clearTimeout(handler); + }, [value, delay]); + + return debouncedValue; +} \ No newline at end of file diff --git a/src/hooks/useEventListener.ts b/src/hooks/useEventListener.ts new file mode 100644 index 0000000..aab6c8e --- /dev/null +++ b/src/hooks/useEventListener.ts @@ -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( + eventName: K, + handler: (event: MediaQueryListEventMap[K]) => void, + element: RefObject, + options?: boolean | AddEventListenerOptions, +): void; + +// Window Event based useEventListener interface +function useEventListener( + 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, + options?: boolean | AddEventListenerOptions, +): void; + +// Document Event based useEventListener interface +function useEventListener( + eventName: K, + handler: (event: DocumentEventMap[K]) => void, + element: RefObject, + 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, + 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; \ No newline at end of file diff --git a/src/hooks/useSSESubscription.ts b/src/hooks/useSSESubscription.ts new file mode 100644 index 0000000..9a55a2d --- /dev/null +++ b/src/hooks/useSSESubscription.ts @@ -0,0 +1,38 @@ +import { useEffect } from "react"; +import ReconnectingEventSource from "reconnecting-eventsource"; +import { devlog } from "../utils"; + + +export function useSSESubscription({ 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]); +} \ No newline at end of file diff --git a/src/hooks/useThrottle.ts b/src/hooks/useThrottle.ts new file mode 100644 index 0000000..55279f8 --- /dev/null +++ b/src/hooks/useThrottle.ts @@ -0,0 +1,22 @@ +import { useState, useEffect, useRef } from "react"; + + +export function useThrottle(value: T, delay: number) { + const [throttledValue, setThrottledValue] = useState(value); + const time = useRef(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; +} \ No newline at end of file diff --git a/src/hooks/useTicketMessages.ts b/src/hooks/useTicketMessages.ts new file mode 100644 index 0000000..f14f0db --- /dev/null +++ b/src/hooks/useTicketMessages.ts @@ -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; + 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({ + 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; +} \ No newline at end of file diff --git a/src/hooks/useTickets.ts b/src/hooks/useTickets.ts new file mode 100644 index 0000000..e6b2bda --- /dev/null +++ b/src/hooks/useTickets.ts @@ -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; + 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({ + 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; +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..e20bcb7 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,6 @@ +export * from "./api"; +export * from "./hooks"; +export * from "./decorators"; +export * from "./utils"; + +export type * from "./model"; \ No newline at end of file diff --git a/src/model/index.ts b/src/model/index.ts new file mode 100644 index 0000000..dff6461 --- /dev/null +++ b/src/model/index.ts @@ -0,0 +1 @@ +export type * from "./ticket"; \ No newline at end of file diff --git a/src/model/ticket.ts b/src/model/ticket.ts new file mode 100644 index 0000000..f7ea7d2 --- /dev/null +++ b/src/model/ticket.ts @@ -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[]; diff --git a/src/utils/backendMessageHandler.ts b/src/utils/backendMessageHandler.ts new file mode 100644 index 0000000..ecc5872 --- /dev/null +++ b/src/utils/backendMessageHandler.ts @@ -0,0 +1,29 @@ +import { isAxiosError } from "axios"; +import { devlog } from "./devlog"; + + +const backendErrorMessage: Record = { + "user not found": "Пользователь не найден", + "invalid password": "Неправильный пароль", + "field is empty": "Поле \"Пароль\" не заполнено", + "field is empty": "Поле \"Логин\" не заполнено", + "field is empty": "Поле \"E-mail\" не заполнено", + "field 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 ?? "Что-то пошло не так. Повторите попытку позже"; +} \ No newline at end of file diff --git a/src/utils/devlog.ts b/src/utils/devlog.ts new file mode 100644 index 0000000..a497bb8 --- /dev/null +++ b/src/utils/devlog.ts @@ -0,0 +1,3 @@ +export const devlog: typeof console.log = (...args) => { + if (process.env.NODE_ENV === "develpment") console.log(...args); +}; \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..21f9936 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from "./devlog"; +export * from "./backendMessageHandler"; \ No newline at end of file diff --git a/themes/theme.tsx b/themes/theme.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ec1803d --- /dev/null +++ b/tsconfig.json @@ -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" + ] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..70827b8 --- /dev/null +++ b/yarn.lock @@ -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==