Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
56 lines
2.0 KiB
TypeScript
56 lines
2.0 KiB
TypeScript
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]);
|
||
}
|