add files from official template

This commit is contained in:
vchikalkin 2024-12-10 20:08:38 +03:00
parent db2727bed6
commit 9ab9880c0f
39 changed files with 1562 additions and 35 deletions

View File

@ -0,0 +1,3 @@
body {
background: var(--tg-theme-secondary-bg-color, white);
}

View File

@ -0,0 +1 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M14.1839 17.7069C13.6405 18.6507 13.3688 19.1226 13.0591 19.348C12.4278 19.8074 11.5723 19.8074 10.941 19.348C10.6312 19.1226 10.3595 18.6507 9.81613 17.7069L5.52066 10.2464C4.76864 8.94024 4.39263 8.28717 4.33762 7.75894C4.2255 6.68236 4.81894 5.65591 5.80788 5.21589C6.29309 5 7.04667 5 8.55383 5H15.4462C16.9534 5 17.7069 5 18.1922 5.21589C19.1811 5.65591 19.7745 6.68236 19.6624 7.75894C19.6074 8.28717 19.2314 8.94024 18.4794 10.2464L14.1839 17.7069ZM11.1 16.3412L6.56139 8.48002C6.31995 8.06185 6.19924 7.85276 6.18146 7.68365C6.14523 7.33896 6.33507 7.01015 6.65169 6.86919C6.80703 6.80002 7.04847 6.80002 7.53133 6.80002H7.53134L11.1 6.80002V16.3412ZM12.9 16.3412L17.4387 8.48002C17.6801 8.06185 17.8008 7.85276 17.8186 7.68365C17.8548 7.33896 17.665 7.01015 17.3484 6.86919C17.193 6.80002 16.9516 6.80002 16.4687 6.80002L12.9 6.80002V16.3412Z" fill="#ffffff"></path></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

5
apps/web/app/error.tsx Normal file
View File

@ -0,0 +1,5 @@
'use client';
import { ErrorPage } from '@/components/ErrorPage';
export default ErrorPage;

BIN
apps/web/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,121 @@
'use client';
import { useMemo } from 'react';
import { useSignal, initData, type User } from '@telegram-apps/sdk-react';
import { List, Placeholder } from '@telegram-apps/telegram-ui';
import {
DisplayData,
type DisplayDataRow,
} from '@/components/DisplayData/DisplayData';
import { Page } from '@/components/Page';
function getUserRows(user: User): DisplayDataRow[] {
return [
{ title: 'id', value: user.id.toString() },
{ title: 'username', value: user.username },
{ title: 'photo_url', value: user.photoUrl },
{ title: 'last_name', value: user.lastName },
{ title: 'first_name', value: user.firstName },
{ title: 'is_bot', value: user.isBot },
{ title: 'is_premium', value: user.isPremium },
{ title: 'language_code', value: user.languageCode },
{ title: 'allows_to_write_to_pm', value: user.allowsWriteToPm },
{ title: 'added_to_attachment_menu', value: user.addedToAttachmentMenu },
];
}
export default function InitDataPage() {
const initDataRaw = useSignal(initData.raw);
const initDataState = useSignal(initData.state);
const initDataRows = useMemo<DisplayDataRow[] | undefined>(() => {
if (!initDataState || !initDataRaw) {
return;
}
const {
authDate,
hash,
queryId,
chatType,
chatInstance,
canSendAfter,
startParam,
} = initDataState;
return [
{ title: 'raw', value: initDataRaw },
{ title: 'auth_date', value: authDate.toLocaleString() },
{ title: 'auth_date (raw)', value: authDate.getTime() / 1000 },
{ title: 'hash', value: hash },
{
title: 'can_send_after',
value: initData.canSendAfterDate()?.toISOString(),
},
{ title: 'can_send_after (raw)', value: canSendAfter },
{ title: 'query_id', value: queryId },
{ title: 'start_param', value: startParam },
{ title: 'chat_type', value: chatType },
{ title: 'chat_instance', value: chatInstance },
];
}, [initDataState, initDataRaw]);
const userRows = useMemo<DisplayDataRow[] | undefined>(() => {
return initDataState && initDataState.user
? getUserRows(initDataState.user)
: undefined;
}, [initDataState]);
const receiverRows = useMemo<DisplayDataRow[] | undefined>(() => {
return initDataState && initDataState.receiver
? getUserRows(initDataState.receiver)
: undefined;
}, [initDataState]);
const chatRows = useMemo<DisplayDataRow[] | undefined>(() => {
if (!initDataState?.chat) {
return;
}
const {
id,
title,
type,
username,
photoUrl,
} = initDataState.chat;
return [
{ title: 'id', value: id.toString() },
{ title: 'title', value: title },
{ title: 'type', value: type },
{ title: 'username', value: username },
{ title: 'photo_url', value: photoUrl },
];
}, [initData]);
if (!initDataRows) {
return (
<Page>
<Placeholder
header="Oops"
description="Application was launched with missing init data"
>
<img
alt="Telegram sticker"
src="https://xelene.me/telegram.gif"
style={{ display: 'block', width: '144px', height: '144px' }}
/>
</Placeholder>
</Page>
);
}
return (
<Page>
<List>
<DisplayData header={'Init Data'} rows={initDataRows}/>
{userRows && <DisplayData header={'User'} rows={userRows}/>}
{receiverRows && <DisplayData header={'Receiver'} rows={receiverRows}/>}
{chatRows && <DisplayData header={'Chat'} rows={chatRows}/>}
</List>
</Page>
);
};

View File

@ -0,0 +1,33 @@
'use client';
import { useLaunchParams } from '@telegram-apps/sdk-react';
import { List } from '@telegram-apps/telegram-ui';
import { DisplayData } from '@/components/DisplayData/DisplayData';
import { Page } from '@/components/Page';
export default function LaunchParamsPage() {
const lp = useLaunchParams();
return (
<Page>
<List>
<DisplayData
rows={[
{ title: 'tgWebAppPlatform', value: lp.platform },
{ title: 'tgWebAppShowSettings', value: lp.showSettings },
{ title: 'tgWebAppVersion', value: lp.version },
{ title: 'tgWebAppBotInline', value: lp.botInline },
{ title: 'tgWebAppStartParam', value: lp.startParam },
{ title: 'tgWebAppData', type: 'link', value: '/init-data' },
{
title: 'tgWebAppThemeParams',
type: 'link',
value: '/theme-params',
},
]}
/>
</List>
</Page>
);
};

View File

@ -1,22 +1,26 @@
import '@repo/ui/globals.css';
import { Root } from '@/components/Root/Root';
import { I18nProvider } from '@/core/i18n/provider';
import { type Metadata } from 'next';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
import { getLocale } from 'next-intl/server';
import { type PropsWithChildren } from 'react';
import '@telegram-apps/telegram-ui/dist/styles.css';
import './_assets/globals.css';
export const metadata: Metadata = {
description: 'Generated by create turbo',
title: 'Docs',
description: 'Your application description goes here',
title: 'Your Application Title Goes Here',
};
export default function RootLayout({
children,
}: {
readonly children: React.ReactNode;
}): JSX.Element {
export default async function RootLayout({ children }: PropsWithChildren) {
const locale = await getLocale();
return (
<html lang="ru">
<body className={inter.className}>{children}</body>
<html lang={locale}>
<body>
<I18nProvider>
<Root>{children}</Root>
</I18nProvider>
</body>
</html>
);
}

View File

@ -1,8 +0,0 @@
import Page from './page';
import { render, screen } from '@testing-library/react';
import { expect, test } from 'vitest';
test('Page', () => {
render(<Page />);
expect(screen.getByRole('button', { name: 'Click me' })).toBeDefined();
});

View File

@ -1,17 +1,35 @@
'use client';
import { Button } from '@repo/ui/components/ui/button';
import { useState } from 'react';
export default function Page() {
const [clicked, setClicked] = useState(false);
import { Link } from '@/components/Link/Link';
import { LocaleSwitcher } from '@/components/LocaleSwitcher/LocaleSwitcher';
import { Page } from '@/components/Page';
import { Cell, List, Section } from '@telegram-apps/telegram-ui';
import { useTranslations } from 'next-intl';
function handleClick() {
setClicked(!clicked);
}
export default function Home() {
const t = useTranslations('i18n');
return (
<main>
<Button onClick={() => handleClick()}>{clicked ? 'Clicked' : 'Click me'}</Button>
</main>
<Page back={false}>
<List>
<Section
footer="These pages help developer to learn more about current launch information"
header="Application Launch Data"
>
<Link href="/init-data">
<Cell subtitle="User data, chat information, technical data">Init Data</Cell>
</Link>
<Link href="/launch-params">
<Cell subtitle="Platform identifier, Mini Apps version, etc.">Launch Parameters</Cell>
</Link>
<Link href="/theme-params">
<Cell subtitle="Telegram application palette information">Theme Parameters</Cell>
</Link>
</Section>
<Section footer={t('footer')} header={t('header')}>
<LocaleSwitcher />
</Section>
</List>
</Page>
);
}

View File

@ -0,0 +1,30 @@
'use client';
import { useSignal, themeParams } from '@telegram-apps/sdk-react';
import { List } from '@telegram-apps/telegram-ui';
import { DisplayData } from '@/components/DisplayData/DisplayData';
import { Page } from '@/components/Page';
export default function ThemeParamsPage() {
const tp = useSignal(themeParams.state);
return (
<Page>
<List>
<DisplayData
rows={
Object
.entries(tp)
.map(([title, value]) => ({
title: title
.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`)
.replace(/background/, 'bg'),
value,
}))
}
/>
</List>
</Page>
);
};

View File

@ -0,0 +1,105 @@
'use client';
import { openLink } from '@telegram-apps/sdk-react';
import { Page } from '@/components/Page';
import { TonConnectButton, useTonWallet } from '@tonconnect/ui-react';
import {
Avatar,
Cell,
List,
Navigation,
Placeholder,
Section,
Text,
Title,
} from '@telegram-apps/telegram-ui';
import { DisplayData } from '@/components/DisplayData/DisplayData';
import './styles.css';
export default function TONConnectPage() {
const wallet = useTonWallet();
if (!wallet) {
return (
<Page>
<Placeholder
className="ton-connect-page__placeholder"
header="TON Connect"
description={
<>
<Text>
To display the data related to the TON Connect, it is required
to connect your wallet
</Text>
<TonConnectButton className="ton-connect-page__button"/>
</>
}
/>
</Page>
);
}
const {
account: { chain, publicKey, address },
device: {
appName,
appVersion,
maxProtocolVersion,
platform,
features,
},
} = wallet;
return (
<Page>
<List>
{'imageUrl' in wallet && (
<>
<Section>
<Cell
before={
<Avatar src={wallet.imageUrl} alt="Provider logo" width={60}
height={60}/>
}
after={<Navigation>About wallet</Navigation>}
subtitle={wallet.appName}
onClick={(e) => {
e.preventDefault();
openLink(wallet.aboutUrl);
}}
>
<Title level="3">{wallet.name}</Title>
</Cell>
</Section>
<TonConnectButton className="ton-connect-page__button-connected"/>
</>
)}
<DisplayData
header="Account"
rows={[
{ title: 'Address', value: address },
{ title: 'Chain', value: chain },
{ title: 'Public Key', value: publicKey },
]}
/>
<DisplayData
header="Device"
rows={[
{ title: 'App Name', value: appName },
{ title: 'App Version', value: appVersion },
{ title: 'Max Protocol Version', value: maxProtocolVersion },
{ title: 'Platform', value: platform },
{
title: 'Features',
value: features
.map(f => typeof f === 'object' ? f.name : undefined)
.filter(v => v)
.join(', '),
},
]}
/>
</List>
</Page>
);
};

View File

@ -0,0 +1,16 @@
.ton-connect-page__placeholder {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
}
.ton-connect-page__button {
margin: 16px auto 0;
}
.ton-connect-page__button-connected {
margin: 16px 24px 16px auto;
}

View File

@ -0,0 +1,59 @@
import { isRGB } from '@telegram-apps/sdk-react';
import { Cell, Checkbox, Section } from '@telegram-apps/telegram-ui';
import type { FC, ReactNode } from 'react';
import { RGB } from '@/components/RGB/RGB';
import { Link } from '@/components/Link/Link';
import './styles.css';
export type DisplayDataRow =
& { title: string }
& (
| { type: 'link'; value?: string }
| { value: ReactNode }
)
export interface DisplayDataProps {
header?: ReactNode;
footer?: ReactNode;
rows: DisplayDataRow[];
}
export const DisplayData: FC<DisplayDataProps> = ({ header, rows }) => (
<Section header={header}>
{rows.map((item, idx) => {
let valueNode: ReactNode;
if (item.value === undefined) {
valueNode = <i>empty</i>;
} else {
if ('type' in item) {
valueNode = <Link href={item.value}>Open</Link>;
} else if (typeof item.value === 'string') {
valueNode = isRGB(item.value)
? <RGB color={item.value}/>
: item.value;
} else if (typeof item.value === 'boolean') {
valueNode = <Checkbox checked={item.value} disabled/>;
} else {
valueNode = item.value;
}
}
return (
<Cell
className='display-data__line'
subhead={item.title}
readOnly
multiline={true}
key={idx}
>
<span className='display-data__line-value'>
{valueNode}
</span>
</Cell>
);
})}
</Section>
);

View File

@ -0,0 +1,15 @@
.display-data__header {
font-weight: 400;
}
.display-data__line {
padding: 16px 24px;
}
.display-data__line-title {
color: var(--tg-theme-subtitle-text-color);
}
.display-data__line-value {
word-break: break-word;
}

View File

@ -0,0 +1,39 @@
import {
Component,
type ComponentType,
type GetDerivedStateFromError,
type PropsWithChildren,
} from 'react';
export interface ErrorBoundaryProps extends PropsWithChildren {
fallback: ComponentType<{ error: Error }>;
}
interface ErrorBoundaryState {
error?: Error;
}
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
state: ErrorBoundaryState = {};
// eslint-disable-next-line max-len
static getDerivedStateFromError: GetDerivedStateFromError<ErrorBoundaryProps, ErrorBoundaryState> = (error) => ({ error });
componentDidCatch(error: Error) {
this.setState({ error });
}
render() {
const {
state: {
error,
},
props: {
fallback: Fallback,
children,
},
} = this;
return error ? <Fallback error={error}/> : children;
}
}

View File

@ -0,0 +1,26 @@
import { useEffect } from 'react';
export function ErrorPage({
error,
reset,
}: {
error: Error & { digest?: string }
reset?: () => void
}) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error);
}, [error]);
return (
<div>
<h2>An unhandled error occurred!</h2>
<blockquote>
<code>
{error.message}
</code>
</blockquote>
{reset && <button onClick={() => reset()}>Try again</button>}
</div>
);
}

View File

@ -0,0 +1,48 @@
import { openLink, classNames } from '@telegram-apps/sdk-react';
import { type FC, type MouseEventHandler, type JSX, useCallback } from 'react';
import { type LinkProps as NextLinkProps, default as NextLink } from 'next/link';
import './styles.css';
export interface LinkProps extends NextLinkProps, Omit<JSX.IntrinsicElements['a'], 'href'> {
}
export const Link: FC<LinkProps> = ({
className,
onClick: propsOnClick,
href,
...rest
}) => {
const onClick = useCallback<MouseEventHandler<HTMLAnchorElement>>((e) => {
propsOnClick?.(e);
// Compute if target path is external. In this case we would like to open link using
// TMA method.
let path: string;
if (typeof href === 'string') {
path = href;
} else {
const { search = '', pathname = '', hash = '' } = href;
path = `${pathname}?${search}#${hash}`;
}
const targetUrl = new URL(path, window.location.toString());
const currentUrl = new URL(window.location.toString());
const isExternal = targetUrl.protocol !== currentUrl.protocol
|| targetUrl.host !== currentUrl.host;
if (isExternal) {
e.preventDefault();
openLink(targetUrl.toString());
}
}, [href, propsOnClick]);
return (
<NextLink
{...rest}
href={href}
onClick={onClick}
className={classNames(className, 'link')}
/>
);
};

View File

@ -0,0 +1,4 @@
.link {
text-decoration: none;
color: var(--tg-theme-link-color);
}

View File

@ -0,0 +1,26 @@
'use client';
import { Select } from '@telegram-apps/telegram-ui';
import { useLocale } from 'next-intl';
import { FC } from 'react';
import { localesMap } from '@/core/i18n/config';
import { setLocale } from '@/core/i18n/locale';
import { Locale } from '@/core/i18n/types';
export const LocaleSwitcher: FC = () => {
const locale = useLocale();
const onChange = (value: string) => {
const locale = value as Locale;
setLocale(locale);
};
return (
<Select value={locale} onChange={({ target }) => onChange(target.value)}>
{localesMap.map((locale) => (
<option key={locale.key} value={locale.key}>{locale.title}</option>
))}
</Select>
);
};

View File

@ -0,0 +1,31 @@
'use client';
import { backButton } from '@telegram-apps/sdk-react';
import { PropsWithChildren, useEffect } from 'react';
import { useRouter } from 'next/navigation';
export function Page({ children, back = true }: PropsWithChildren<{
/**
* True if it is allowed to go back from this page.
* @default true
*/
back?: boolean
}>) {
const router = useRouter();
useEffect(() => {
if (back) {
backButton.show();
} else {
backButton.hide();
}
}, [back]);
useEffect(() => {
return backButton.onClick(() => {
router.back();
});
}, [router]);
return <>{children}</>;
}

View File

@ -0,0 +1,15 @@
import { classNames, type RGB as RGBType } from '@telegram-apps/sdk-react';
import type { FC } from 'react';
import './styles.css';
export type RGBProps = JSX.IntrinsicElements['div'] & {
color: RGBType;
};
export const RGB: FC<RGBProps> = ({ color, className, ...rest }) => (
<span {...rest} className={classNames('rgb', className)}>
<i className='rgb__icon' style={{ backgroundColor: color }}/>
{color}
</span>
);

View File

@ -0,0 +1,12 @@
.rgb {
display: inline-flex;
align-items: center;
gap: 5px;
}
.rgb__icon {
width: 18px;
aspect-ratio: 1;
border: 1px solid #555;
border-radius: 50%;
}

View File

@ -0,0 +1,63 @@
'use client';
import { ErrorBoundary } from '@/components/ErrorBoundary';
import { ErrorPage } from '@/components/ErrorPage';
import { setLocale } from '@/core/i18n/locale';
import { init } from '@/core/init';
import { useClientOnce } from '@/hooks/useClientOnce';
import { useDidMount } from '@/hooks/useDidMount';
import { useTelegramMock } from '@/hooks/useTelegramMock';
import { initData, miniApp, useLaunchParams, useSignal } from '@telegram-apps/sdk-react';
import { AppRoot } from '@telegram-apps/telegram-ui';
import { type PropsWithChildren, useEffect } from 'react';
import './styles.css';
export function Root(props: PropsWithChildren) {
// Unfortunately, Telegram Mini Apps does not allow us to use all features of
// the Server Side Rendering. That's why we are showing loader on the server
// side.
const didMount = useDidMount();
return didMount ? (
<ErrorBoundary fallback={ErrorPage}>
<RootInner {...props} />
</ErrorBoundary>
) : (
<div className="root__loading">Loading</div>
);
}
function RootInner({ children }: PropsWithChildren) {
const isDevelopment = process.env.NODE_ENV === 'development';
// Mock Telegram environment in development mode if needed.
if (isDevelopment) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useTelegramMock();
}
const lp = useLaunchParams();
const debug = isDevelopment || lp.startParam === 'debug';
// Initialize the library.
useClientOnce(() => {
init(debug);
});
const isDark = useSignal(miniApp.isDark);
const initDataUser = useSignal(initData.user);
// Set the user locale.
useEffect(() => {
initDataUser && setLocale(initDataUser.languageCode);
}, [initDataUser]);
return (
<AppRoot
appearance={isDark ? 'dark' : 'light'}
platform={['ios', 'macos'].includes(lp.platform) ? 'ios' : 'base'}
>
{children}
</AppRoot>
);
}

View File

@ -0,0 +1,10 @@
.root__loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}

View File

@ -0,0 +1,10 @@
export const defaultLocale = 'en';
export const timeZone = 'Europe/Amsterdam';
export const locales = [defaultLocale, 'ru'] as const;
export const localesMap = [
{ key: 'en', title: 'English' },
{ key: 'ru', title: 'Русский' },
];

View File

@ -0,0 +1,18 @@
import { defaultLocale, locales } from './config';
import { getLocale } from './locale';
import { type Locale } from './types';
import { getRequestConfig } from 'next-intl/server';
const i18nRequestConfig = getRequestConfig(async () => {
const locale = (await getLocale()) as Locale;
return {
locale,
messages:
locale === defaultLocale || !locales.includes(locale)
? (await import(`@/public/locales/${defaultLocale}.json`)).default
: (await import(`@/public/locales/${locale}.json`)).default,
};
});
export default i18nRequestConfig;

View File

@ -0,0 +1,21 @@
//use server is required
"use server";
import { cookies } from "next/headers";
import { defaultLocale } from "./config";
import type { Locale } from "./types";
// In this example the locale is read from a cookie. You could alternatively
// also read it from a database, backend service, or any other source.
const COOKIE_NAME = "NEXT_LOCALE";
const getLocale = async () => {
return cookies().get(COOKIE_NAME)?.value || defaultLocale;
};
const setLocale = async (locale?: string) => {
cookies().set(COOKIE_NAME, locale as Locale || defaultLocale);
};
export { getLocale, setLocale };

View File

@ -0,0 +1,18 @@
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
import React from "react";
import { timeZone } from "./config";
const I18nProvider: React.FC<React.PropsWithChildren> = async ({
children,
}) => {
const messages = await getMessages();
return (
<NextIntlClientProvider messages={messages} timeZone={timeZone}>
{children}
</NextIntlClientProvider>
);
};
export { I18nProvider };

View File

@ -0,0 +1,5 @@
import type { locales } from "./config";
type Locale = (typeof locales)[number];
export type { Locale };

41
apps/web/core/init.ts Normal file
View File

@ -0,0 +1,41 @@
import {
$debug,
backButton,
initData,
init as initSDK,
miniApp,
themeParams,
viewport,
} from '@telegram-apps/sdk-react';
/**
* Initializes the application and configures its dependencies.
*/
export function init(debug: boolean): void {
// Set @telegram-apps/sdk-react debug mode.
$debug.set(debug);
// Initialize special event handlers for Telegram Desktop, Android, iOS, etc.
// Also, configure the package.
initSDK();
// Mount all components used in the project.
if (backButton.isSupported()) backButton.mount();
miniApp.mount();
themeParams.mount();
initData.restore();
void viewport
.mount()
.then(() => {
// Define components-related CSS variables.
viewport.bindCssVars();
miniApp.bindCssVars();
themeParams.bindCssVars();
})
.catch((error) => {
console.error('Something went wrong mounting the viewport', error);
});
// Add Eruda if needed.
debug && import('eruda').then((library) => library.default.init()).catch(console.error);
}

View File

@ -0,0 +1,9 @@
import { useRef } from 'react';
export function useClientOnce(fn: () => void): void {
const canCall = useRef(true);
if (typeof window !== 'undefined' && canCall.current) {
canCall.current = false;
fn();
}
}

View File

@ -0,0 +1,14 @@
import { useEffect, useState } from 'react';
/**
* @return True, if component was mounted.
*/
export function useDidMount(): boolean {
const [didMount, setDidMount] = useState(false);
useEffect(() => {
setDidMount(true);
}, []);
return didMount;
}

View File

@ -0,0 +1,75 @@
import { useClientOnce } from '@/hooks/useClientOnce';
import {
isTMA,
type LaunchParams,
mockTelegramEnv,
parseInitData,
retrieveLaunchParams,
} from '@telegram-apps/sdk-react';
/**
* Mocks Telegram environment in development mode.
*/
export function useTelegramMock(): void {
useClientOnce(() => {
if (!sessionStorage.getItem('env-mocked') && isTMA('simple')) {
return;
}
// Determine which launch params should be applied. We could already
// apply them previously, or they may be specified on purpose using the
// default launch parameters transmission method.
let lp: LaunchParams | undefined;
try {
lp = retrieveLaunchParams();
} catch {
const initDataRaw = new URLSearchParams([
[
'user',
JSON.stringify({
allows_write_to_pm: true,
first_name: 'Andrew',
id: 99_281_932,
is_premium: true,
language_code: 'en',
last_name: 'Rogue',
username: 'rogue',
}),
],
['hash', '89d6079ad6762351f38c6dbbc41bb53048019256a9443988af7a48bcad16ba31'],
['auth_date', '1716922846'],
['start_param', 'debug'],
['chat_type', 'sender'],
['chat_instance', '8428209589180549439'],
]).toString();
lp = {
initData: parseInitData(initDataRaw),
initDataRaw,
platform: 'tdesktop',
themeParams: {
accentTextColor: '#6ab2f2',
bgColor: '#17212b',
buttonColor: '#5288c1',
buttonTextColor: '#ffffff',
destructiveTextColor: '#ec3942',
headerBgColor: '#17212b',
hintColor: '#708499',
linkColor: '#6ab3f3',
secondaryBgColor: '#232e3c',
sectionBgColor: '#17212b',
sectionHeaderTextColor: '#6ab3f3',
subtitleTextColor: '#708499',
textColor: '#f5f5f5',
},
version: '8',
};
}
sessionStorage.setItem('env-mocked', '1');
mockTelegramEnv(lp);
console.warn(
'⚠️ As long as the current environment was not considered as the Telegram-based one, it was mocked. Take a note, that you should not do it in production and current behavior is only specific to the development process. Environment mocking is also applied only in development mode. So, after building the application, you will not see this behavior and related warning, leading to crashing the application outside Telegram.',
);
});
}

View File

@ -1,7 +1,10 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin('./core/i18n/i18n.ts');
const nextConfig = withNextIntl({
reactStrictMode: true,
transpilePackages: ['@repo/ui'],
};
});
export default nextConfig;

View File

@ -17,8 +17,11 @@
"dependencies": {
"@apollo/client": "catalog:",
"@repo/ui": "workspace:*",
"@telegram-apps/sdk-react": "^2.0.19",
"@telegram-apps/telegram-ui": "^2.1.8",
"graphql": "catalog:",
"next": "catalog:",
"next-intl": "^3.26.0",
"react": "catalog:",
"react-dom": "catalog:"
},
@ -38,6 +41,7 @@
"@types/react-dom": "catalog:",
"@vitejs/plugin-react": "catalog:",
"autoprefixer": "catalog:",
"eruda": "^3.4.1",
"jsdom": "catalog:",
"lint-staged": "catalog:",
"postcss": "catalog:",

View File

@ -0,0 +1,6 @@
{
"i18n": {
"header": "Application supports i18n",
"footer": "You can select a different language from the dropdown menu."
}
}

View File

@ -0,0 +1,6 @@
{
"i18n": {
"header": "Поддержка i18n",
"footer": "Вы можете выбрать другой язык в выпадающем меню."
}
}

View File

@ -5,7 +5,11 @@
{
"name": "next"
}
]
],
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",

617
pnpm-lock.yaml generated
View File

@ -143,12 +143,21 @@ importers:
'@repo/ui':
specifier: workspace:*
version: link:../../packages/ui
'@telegram-apps/sdk-react':
specifier: ^2.0.19
version: 2.0.19(@types/react@19.0.1)(react@19.0.0)
'@telegram-apps/telegram-ui':
specifier: ^2.1.8
version: 2.1.8(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
graphql:
specifier: 'catalog:'
version: 16.9.0
next:
specifier: 'catalog:'
version: 15.0.4(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next-intl:
specifier: ^3.26.0
version: 3.26.0(next@15.0.4(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
react:
specifier: 'catalog:'
version: 19.0.0
@ -201,6 +210,9 @@ importers:
autoprefixer:
specifier: 'catalog:'
version: 10.4.20(postcss@8.4.49)
eruda:
specifier: ^3.4.1
version: 3.4.1
jsdom:
specifier: 'catalog:'
version: 25.0.1
@ -1123,6 +1135,36 @@ packages:
resolution: {integrity: sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@floating-ui/core@1.6.8':
resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==}
'@floating-ui/dom@1.6.12':
resolution: {integrity: sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==}
'@floating-ui/react-dom@2.1.2':
resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@floating-ui/utils@0.2.8':
resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==}
'@formatjs/ecma402-abstract@2.3.1':
resolution: {integrity: sha512-Ip9uV+/MpLXWRk03U/GzeJMuPeOXpJBSB5V1tjA6kJhvqssye5J5LoYLc7Z5IAHb7nR62sRoguzrFiVCP/hnzw==}
'@formatjs/fast-memoize@2.2.5':
resolution: {integrity: sha512-6PoewUMrrcqxSoBXAOJDiW1m+AmkrAj0RiXnOMD59GRaswjXhm3MDhgepXPBgonc09oSirAJTsAggzAGQf6A6g==}
'@formatjs/icu-messageformat-parser@2.9.7':
resolution: {integrity: sha512-cuEHyRM5VqLQobANOjtjlgU7+qmk9Q3fDQuBiRRJ3+Wp3ZoZhpUPtUfuimZXsir6SaI2TaAJ+SLo9vLnV5QcbA==}
'@formatjs/icu-skeleton-parser@1.8.11':
resolution: {integrity: sha512-8LlHHE/yL/zVJZHAX3pbKaCjZKmBIO6aJY1mkVh4RMSEu/2WRZ4Ysvv3kKXJ9M8RJLBHdnk1/dUQFdod1Dt7Dw==}
'@formatjs/intl-localematcher@0.5.9':
resolution: {integrity: sha512-8zkGu/sv5euxbjfZ/xmklqLyDGQSxsLqg8XOq88JW3cmJtzhCP8EtSJXlaKZnVO4beEaoiT9wj4eIoCQ9smwxA==}
'@graphql-codegen/add@5.0.3':
resolution: {integrity: sha512-SxXPmramkth8XtBlAHu4H4jYcYXM/o3p01+psU+0NADQowA8jtYkK6MW5rV6T+CxkEaNZItfSmZRPgIuypcqnA==}
peerDependencies:
@ -1681,6 +1723,9 @@ packages:
engines: {node: '>=18'}
hasBin: true
'@radix-ui/primitive@1.1.0':
resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==}
'@radix-ui/react-compose-refs@1.1.0':
resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==}
peerDependencies:
@ -1690,6 +1735,111 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-context@1.1.1':
resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-dialog@1.1.2':
resolution: {integrity: sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-dismissable-layer@1.1.1':
resolution: {integrity: sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-focus-guards@1.1.1':
resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-focus-scope@1.1.0':
resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-id@1.1.0':
resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-portal@1.1.2':
resolution: {integrity: sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-presence@1.1.1':
resolution: {integrity: sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@2.0.0':
resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-slot@1.1.0':
resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==}
peerDependencies:
@ -1699,6 +1849,42 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-callback-ref@1.1.0':
resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-controllable-state@1.1.0':
resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-escape-keydown@1.1.0':
resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-layout-effect@1.1.0':
resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@repeaterjs/repeater@3.0.4':
resolution: {integrity: sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA==}
@ -1812,6 +1998,43 @@ packages:
'@swc/helpers@0.5.13':
resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==}
'@telegram-apps/bridge@1.7.1':
resolution: {integrity: sha512-oRbznpIC4UibMVygQ+tcS0ZSKx7DaI07MXQF42VETQ/VOCKeaWZeQFUifo4A+CzT6XMGo2hyse/CQP9ziX0H7g==}
'@telegram-apps/navigation@1.0.9':
resolution: {integrity: sha512-Ur24luu+fizrKCDQAoJWQzMj+IwNiqtQlrITz42DORKSohj5yvf9kD5AJO2r9sHC+iC2pLXHCn1dV34o6tbeaQ==}
'@telegram-apps/sdk-react@2.0.19':
resolution: {integrity: sha512-mQfBspQd+KbNW9gp26b2QN+T+SFiYfjZV1X91mM68hIPnChPjwCJRTZrNtx+nvT49B/NURMMtTT/eF84IX1ltA==}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@telegram-apps/sdk@2.9.1':
resolution: {integrity: sha512-fW5e0B7yt4jIlEJfXzZwrY2cYufyXQw6xIu2eRfKl1l6N/ETIwZZRr6jOOsRCzzpWUfqePvwItqKIuRf8mGcog==}
'@telegram-apps/signals@1.1.0':
resolution: {integrity: sha512-5qN7cU8t3l7n0cKcnzc/1TYKJTwAggUinfwbLHL1SYmB47pBHjCvfsRiYliFohk6lb635SBmNuVZL6LHFmGZaw==}
'@telegram-apps/telegram-ui@2.1.8':
resolution: {integrity: sha512-Y4mIRzBJDerFvF6yzsfwHpu2tKEMHnnnQgdnLTwIdXNVPq/sJTaY66Qxbh9eeO5+tct01VcAx4E70/1fMvnp8A==}
engines: {node: '>=18.0.0', npm: '>=7.0.0'}
peerDependencies:
react: ^18.2.0
react-dom: ^18.2.0
'@telegram-apps/toolkit@1.0.0':
resolution: {integrity: sha512-fSVoveLuMzwRKWeXEufMSXxH+HvjsFKb1DeT3pG5qLpnb2rdtejnNcwAt6WEPtiZ3a4YntYaFuR3KYgVv0ZxeQ==}
'@telegram-apps/transformers@1.2.0':
resolution: {integrity: sha512-RufLdD044RPaAJdh+Mp/98JI+Wkp5mhX3WYCg6IZYFMRwu3QTu2FBwYmU9FdRmBF9utbcymSFrY1cqxh+Vtkfg==}
'@telegram-apps/types@1.2.0':
resolution: {integrity: sha512-HhvnSCsqlJpes5ZGsZP/qbDNq8eLLnjgZKaF5NRsDqAKUPvaIIFT1HdyDII/8EioUgoI4FHsP8MylK2Gzm2efg==}
'@testing-library/dom@10.4.0':
resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
engines: {node: '>=18'}
@ -1831,6 +2054,9 @@ packages:
'@types/react-dom':
optional: true
'@twa-dev/types@7.10.0':
resolution: {integrity: sha512-BXHyNLXy+cPbOZ2qQq5ArPKcP4kUfuX3YBSFT0XUxtMNWAzvyMuYAiq59UTfQ8OnELXlfChrAoIT3c2pujBtcQ==}
'@types/aria-query@5.0.4':
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
@ -2079,6 +2305,12 @@ packages:
resolution: {integrity: sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==}
engines: {node: '>=8'}
'@xelene/vaul-with-scroll-fix@0.1.4':
resolution: {integrity: sha512-R9J7y92rzZKIQLtFHKtzRsb4x8G26cvwGIDSf7OQZCdROFt7xic4Yz1BlFYjm8oiQsYV4SFSQfOzatZjvxz5XQ==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@ -2145,6 +2377,10 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
aria-hidden@1.2.4:
resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==}
engines: {node: '>=10'}
aria-query@5.3.0:
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
@ -2618,6 +2854,9 @@ packages:
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
engines: {node: '>=8'}
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
@ -2684,6 +2923,9 @@ packages:
error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
eruda@3.4.1:
resolution: {integrity: sha512-RmaO5yD97URY/9Q0lye3cmmNPoXNKreeePIw7c/zllbscR92CjGFZFuQ70+0fLIvLcKW3Xha8DS8NFhmeNbEBQ==}
es-abstract@1.23.5:
resolution: {integrity: sha512-vlmniQ0WNPwXqA0BnmwV3Ng7HxiGlh6r5U6JcTMNx8OilcAGqVJBHJcPjqOMaczU9fRuRK5Px2BdVyPRnKMMVQ==}
engines: {node: '>= 0.4'}
@ -3216,6 +3458,10 @@ packages:
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
engines: {node: '>= 0.4'}
get-nonce@1.0.1:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
engines: {node: '>=6'}
get-set-props@0.1.0:
resolution: {integrity: sha512-7oKuKzAGKj0ag+eWZwcGw2fjiZ78tXnXQoBgY0aU7ZOxTu4bB7hSuQSDgtKy978EDH062P5FmD2EWiDpQS9K9Q==}
engines: {node: '>=0.10.0'}
@ -3447,6 +3693,9 @@ packages:
resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==}
engines: {node: '>= 0.4'}
intl-messageformat@10.7.10:
resolution: {integrity: sha512-hp7iejCBiJdW3zmOe18FdlJu8U/JsADSDiBPQhfdSeI8B9POtvPRvPh3nMlvhYayGMKLv6maldhR7y3Pf1vkpw==}
invariant@2.2.4:
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
@ -3935,6 +4184,16 @@ packages:
resolution: {integrity: sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==}
engines: {node: '>=18'}
negotiator@1.0.0:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'}
next-intl@3.26.0:
resolution: {integrity: sha512-gkamnHIANQzeW8xpTGRxd0xiOCztQhY8GDp79fgdlw0GioqrjTEfSWLhHkgaAtvHRbuh/ByJdwiEY5eNK9bUSQ==}
peerDependencies:
next: ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
next@15.0.4:
resolution: {integrity: sha512-nuy8FH6M1FG0lktGotamQDCXhh5hZ19Vo0ht1AOIQWrYJLP598TIUagKtvJrfJ5AGwB/WmDqkKaKhMpVifvGPA==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
@ -4338,6 +4597,36 @@ packages:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'}
react-remove-scroll-bar@2.3.6:
resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
react-remove-scroll@2.6.0:
resolution: {integrity: sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
react-style-singleton@2.2.1:
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
react@19.0.0:
resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
engines: {node: '>=0.10.0'}
@ -5042,6 +5331,31 @@ packages:
urlpattern-polyfill@8.0.2:
resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==}
use-callback-ref@1.3.2:
resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
use-intl@3.26.0:
resolution: {integrity: sha512-HGXmpjGlbEv1uFZPfm557LK8p/hv0pKF9UwnrJeHUTxQx6bUGzMgpmPRLCVY3zkr7hfjy4LPwQJfk4Fhnn+dIg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
use-sidecar@1.1.2:
resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@ -6262,6 +6576,49 @@ snapshots:
dependencies:
levn: 0.4.1
'@floating-ui/core@1.6.8':
dependencies:
'@floating-ui/utils': 0.2.8
'@floating-ui/dom@1.6.12':
dependencies:
'@floating-ui/core': 1.6.8
'@floating-ui/utils': 0.2.8
'@floating-ui/react-dom@2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@floating-ui/dom': 1.6.12
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
'@floating-ui/utils@0.2.8': {}
'@formatjs/ecma402-abstract@2.3.1':
dependencies:
'@formatjs/fast-memoize': 2.2.5
'@formatjs/intl-localematcher': 0.5.9
decimal.js: 10.4.3
tslib: 2.8.1
'@formatjs/fast-memoize@2.2.5':
dependencies:
tslib: 2.8.1
'@formatjs/icu-messageformat-parser@2.9.7':
dependencies:
'@formatjs/ecma402-abstract': 2.3.1
'@formatjs/icu-skeleton-parser': 1.8.11
tslib: 2.8.1
'@formatjs/icu-skeleton-parser@1.8.11':
dependencies:
'@formatjs/ecma402-abstract': 2.3.1
tslib: 2.8.1
'@formatjs/intl-localematcher@0.5.9':
dependencies:
tslib: 2.8.1
'@graphql-codegen/add@5.0.3(graphql@16.9.0)':
dependencies:
'@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.9.0)
@ -7082,12 +7439,108 @@ snapshots:
dependencies:
playwright: 1.49.1
'@radix-ui/primitive@1.1.0': {}
'@radix-ui/react-compose-refs@1.1.0(@types/react@19.0.1)(react@19.0.0)':
dependencies:
react: 19.0.0
optionalDependencies:
'@types/react': 19.0.1
'@radix-ui/react-context@1.1.1(@types/react@19.0.1)(react@19.0.0)':
dependencies:
react: 19.0.0
optionalDependencies:
'@types/react': 19.0.1
'@radix-ui/react-dialog@1.1.2(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.0.0)
'@radix-ui/react-context': 1.1.1(@types/react@19.0.1)(react@19.0.0)
'@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-focus-guards': 1.1.1(@types/react@19.0.1)(react@19.0.0)
'@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-id': 1.1.0(@types/react@19.0.1)(react@19.0.0)
'@radix-ui/react-portal': 1.1.2(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-presence': 1.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-slot': 1.1.0(@types/react@19.0.1)(react@19.0.0)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.1)(react@19.0.0)
aria-hidden: 1.2.4
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
react-remove-scroll: 2.6.0(@types/react@19.0.1)(react@19.0.0)
optionalDependencies:
'@types/react': 19.0.1
'@types/react-dom': 19.0.1
'@radix-ui/react-dismissable-layer@1.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.0.0)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.0.0)
'@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@19.0.1)(react@19.0.0)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
optionalDependencies:
'@types/react': 19.0.1
'@types/react-dom': 19.0.1
'@radix-ui/react-focus-guards@1.1.1(@types/react@19.0.1)(react@19.0.0)':
dependencies:
react: 19.0.0
optionalDependencies:
'@types/react': 19.0.1
'@radix-ui/react-focus-scope@1.1.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.0.0)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.0.0)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
optionalDependencies:
'@types/react': 19.0.1
'@types/react-dom': 19.0.1
'@radix-ui/react-id@1.1.0(@types/react@19.0.1)(react@19.0.0)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.1)(react@19.0.0)
react: 19.0.0
optionalDependencies:
'@types/react': 19.0.1
'@radix-ui/react-portal@1.1.2(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.1)(react@19.0.0)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
optionalDependencies:
'@types/react': 19.0.1
'@types/react-dom': 19.0.1
'@radix-ui/react-presence@1.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.0.0)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.1)(react@19.0.0)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
optionalDependencies:
'@types/react': 19.0.1
'@types/react-dom': 19.0.1
'@radix-ui/react-primitive@2.0.0(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/react-slot': 1.1.0(@types/react@19.0.1)(react@19.0.0)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
optionalDependencies:
'@types/react': 19.0.1
'@types/react-dom': 19.0.1
'@radix-ui/react-slot@1.1.0(@types/react@19.0.1)(react@19.0.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@19.0.1)(react@19.0.0)
@ -7095,6 +7548,32 @@ snapshots:
optionalDependencies:
'@types/react': 19.0.1
'@radix-ui/react-use-callback-ref@1.1.0(@types/react@19.0.1)(react@19.0.0)':
dependencies:
react: 19.0.0
optionalDependencies:
'@types/react': 19.0.1
'@radix-ui/react-use-controllable-state@1.1.0(@types/react@19.0.1)(react@19.0.0)':
dependencies:
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.0.0)
react: 19.0.0
optionalDependencies:
'@types/react': 19.0.1
'@radix-ui/react-use-escape-keydown@1.1.0(@types/react@19.0.1)(react@19.0.0)':
dependencies:
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.1)(react@19.0.0)
react: 19.0.0
optionalDependencies:
'@types/react': 19.0.1
'@radix-ui/react-use-layout-effect@1.1.0(@types/react@19.0.1)(react@19.0.0)':
dependencies:
react: 19.0.0
optionalDependencies:
'@types/react': 19.0.1
'@repeaterjs/repeater@3.0.4': {}
'@repeaterjs/repeater@3.0.6': {}
@ -7174,6 +7653,57 @@ snapshots:
dependencies:
tslib: 2.8.1
'@telegram-apps/bridge@1.7.1':
dependencies:
'@telegram-apps/signals': 1.1.0
'@telegram-apps/toolkit': 1.0.0
'@telegram-apps/transformers': 1.2.0
'@telegram-apps/types': 1.2.0
'@telegram-apps/navigation@1.0.9':
dependencies:
'@telegram-apps/bridge': 1.7.1
'@telegram-apps/signals': 1.1.0
'@telegram-apps/toolkit': 1.0.0
'@telegram-apps/sdk-react@2.0.19(@types/react@19.0.1)(react@19.0.0)':
dependencies:
'@telegram-apps/sdk': 2.9.1
react: 19.0.0
optionalDependencies:
'@types/react': 19.0.1
'@telegram-apps/sdk@2.9.1':
dependencies:
'@telegram-apps/bridge': 1.7.1
'@telegram-apps/navigation': 1.0.9
'@telegram-apps/signals': 1.1.0
'@telegram-apps/toolkit': 1.0.0
'@telegram-apps/transformers': 1.2.0
'@telegram-apps/signals@1.1.0': {}
'@telegram-apps/telegram-ui@2.1.8(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@floating-ui/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@swc/helpers': 0.5.13
'@twa-dev/types': 7.10.0
'@xelene/vaul-with-scroll-fix': 0.1.4(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
transitivePeerDependencies:
- '@types/react'
- '@types/react-dom'
'@telegram-apps/toolkit@1.0.0': {}
'@telegram-apps/transformers@1.2.0':
dependencies:
'@telegram-apps/toolkit': 1.0.0
'@telegram-apps/types': 1.2.0
'@telegram-apps/types@1.2.0': {}
'@testing-library/dom@10.4.0':
dependencies:
'@babel/code-frame': 7.26.2
@ -7195,6 +7725,8 @@ snapshots:
'@types/react': 19.0.1
'@types/react-dom': 19.0.1
'@twa-dev/types@7.10.0': {}
'@types/aria-query@5.0.4': {}
'@types/babel__core@7.20.5':
@ -7541,6 +8073,15 @@ snapshots:
dependencies:
tslib: 2.8.1
'@xelene/vaul-with-scroll-fix@0.1.4(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/react-dialog': 1.1.2(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
transitivePeerDependencies:
- '@types/react'
- '@types/react-dom'
acorn-jsx@5.3.2(acorn@8.14.0):
dependencies:
acorn: 8.14.0
@ -7594,6 +8135,10 @@ snapshots:
argparse@2.0.1: {}
aria-hidden@1.2.4:
dependencies:
tslib: 2.8.1
aria-query@5.3.0:
dependencies:
dequal: 2.0.3
@ -8122,6 +8667,8 @@ snapshots:
detect-libc@2.0.3:
optional: true
detect-node-es@1.1.0: {}
didyoumean@1.2.2: {}
dir-glob@3.0.1:
@ -8176,6 +8723,8 @@ snapshots:
dependencies:
is-arrayish: 0.2.1
eruda@3.4.1: {}
es-abstract@1.23.5:
dependencies:
array-buffer-byte-length: 1.0.1
@ -8993,6 +9542,8 @@ snapshots:
has-symbols: 1.0.3
hasown: 2.0.2
get-nonce@1.0.1: {}
get-set-props@0.1.0: {}
get-stream@8.0.1: {}
@ -9246,6 +9797,13 @@ snapshots:
hasown: 2.0.2
side-channel: 1.0.6
intl-messageformat@10.7.10:
dependencies:
'@formatjs/ecma402-abstract': 2.3.1
'@formatjs/fast-memoize': 2.2.5
'@formatjs/icu-messageformat-parser': 2.9.7
tslib: 2.8.1
invariant@2.2.4:
dependencies:
loose-envify: 1.4.0
@ -9717,6 +10275,16 @@ snapshots:
natural-orderby@5.0.0: {}
negotiator@1.0.0: {}
next-intl@3.26.0(next@15.0.4(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0):
dependencies:
'@formatjs/intl-localematcher': 0.5.9
negotiator: 1.0.0
next: 15.0.4(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: 19.0.0
use-intl: 3.26.0(react@19.0.0)
next@15.0.4(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
'@next/env': 15.0.4
@ -10085,6 +10653,34 @@ snapshots:
react-refresh@0.14.2: {}
react-remove-scroll-bar@2.3.6(@types/react@19.0.1)(react@19.0.0):
dependencies:
react: 19.0.0
react-style-singleton: 2.2.1(@types/react@19.0.1)(react@19.0.0)
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.0.1
react-remove-scroll@2.6.0(@types/react@19.0.1)(react@19.0.0):
dependencies:
react: 19.0.0
react-remove-scroll-bar: 2.3.6(@types/react@19.0.1)(react@19.0.0)
react-style-singleton: 2.2.1(@types/react@19.0.1)(react@19.0.0)
tslib: 2.8.1
use-callback-ref: 1.3.2(@types/react@19.0.1)(react@19.0.0)
use-sidecar: 1.1.2(@types/react@19.0.1)(react@19.0.0)
optionalDependencies:
'@types/react': 19.0.1
react-style-singleton@2.2.1(@types/react@19.0.1)(react@19.0.0):
dependencies:
get-nonce: 1.0.1
invariant: 2.2.4
react: 19.0.0
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.0.1
react@19.0.0: {}
read-cache@1.0.0:
@ -10846,6 +11442,27 @@ snapshots:
urlpattern-polyfill@8.0.2: {}
use-callback-ref@1.3.2(@types/react@19.0.1)(react@19.0.0):
dependencies:
react: 19.0.0
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.0.1
use-intl@3.26.0(react@19.0.0):
dependencies:
'@formatjs/fast-memoize': 2.2.5
intl-messageformat: 10.7.10
react: 19.0.0
use-sidecar@1.1.2(@types/react@19.0.1)(react@19.0.0):
dependencies:
detect-node-es: 1.1.0
react: 19.0.0
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.0.1
util-deprecate@1.0.2: {}
validate-npm-package-license@3.0.4: