Apps, web, i18n, UI, partners, payouts, backoffice, bariki, prod infra

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zabudko-ruandClaude Opus 5.5 committed 2026-10-25 22:55:29 +04:00
1 parent 45d7512d2b
commit 3489e0b816
319 files changed
+45483 -121

No files matched your search

+55
View File
@@ -0,0 +1,55 @@
import { useEffect, useSyncExternalStore } from 'react';
import { tg } from './telegram.ts';
/**
* Минимальный hash-роутер: Mini App открывается по одному URL, а hash переживает
* перезагрузку WebView и не требует настройки сервера.
*/
function subscribe(cb: () => void) {
window.addEventListener('hashchange', cb);
return () => window.removeEventListener('hashchange', cb);
}
// Telegram открывает Mini App с #tgWebAppData=… — это не маршрут, а параметры запуска
const snapshot = () => {
const h = window.location.hash.replace(/^#/, '');
return h.startsWith('/') ? h : '/';
};
export function useLocation() {
const raw = useSyncExternalStore(subscribe, snapshot, () => '/');
const [path, search = ''] = raw.split('?');
return { path: path || '/', query: new URLSearchParams(search) };
}
export function navigate(to: string, opts: { replace?: boolean } = {}) {
if (opts.replace) window.location.replace(`#${to}`);
else window.location.hash = to;
}
/** Сопоставление '/orders/:id' → { id } */
export function match(pattern: string, path: string): Record<string, string> | null {
const p = pattern.split('/').filter(Boolean);
const a = path.split('/').filter(Boolean);
if (p.length !== a.length) return null;
const params: Record<string, string> = {};
for (let i = 0; i < p.length; i++) {
if (p[i]!.startsWith(':')) params[p[i]!.slice(1)] = decodeURIComponent(a[i]!);
else if (p[i] !== a[i]) return null;
}
return params;
}
/** Кнопка «Назад» Telegram на всех экранах, кроме корневых */
export function useTelegramBack(path: string, roots: string[]) {
useEffect(() => {
const back = tg()?.BackButton;
if (!back) return;
const onBack = () => window.history.back();
if (roots.includes(path)) back.hide();
else {
back.show();
back.onClick(onBack);
}
return () => back.offClick(onBack);
}, [path, roots]);
}