add stuff

This commit is contained in:
nflnkr 2023-06-06 13:02:17 +03:00
parent 6ad843a2a0
commit a9ca5294dd
23 changed files with 675 additions and 10 deletions

6
.gitignore vendored Normal file

@ -0,0 +1,6 @@
node_modules
dist
npm-debug.log*
yarn-debug.log*
yarn-error.log*

@ -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 <kotilion.95@gmail.com>",
"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 <kotilion.95@gmail.com>",
"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"
}
}

@ -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

@ -0,0 +1,2 @@
export * from "./createMakeRequest";
export * from "./tickets";

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

@ -0,0 +1 @@
export * from "./throttle";

@ -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

@ -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

@ -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;
}

@ -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;

@ -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

@ -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;
}

@ -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

@ -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

@ -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

@ -0,0 +1 @@
export type * from "./ticket";

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[];

@ -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

@ -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

@ -0,0 +1,2 @@
export * from "./devlog";
export * from "./backendMessageHandler";

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

@ -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==