feat: add StroyBot app shell with URL routing

One web part replaces the separate stroy-bot-* web parts. The section and
its parameters live in the page query string (?page=objects&projectId=5),
so links can be copied, opened in a new tab and the browser Back button works.

- shared config: site/Dataverse/API addresses, list and table names
- HTTP layer behind IHttpClient so repositories can be unit-tested
- ui-kit AppLayout + Sidebar, placeholder for sections not ported yet
- migration checklist with every feature of the old web parts
- design tokens mirrored from ui-kit instead of 8 copies

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-10-15 16:42:37 +05:00
parent 5418da3bdc
commit b868e8012d
37 changed files with 1135 additions and 274 deletions
@@ -0,0 +1,49 @@
import * as React from 'react';
import { buildSearch, IRoute, parseRoute } from './routes';
interface IRouterValue {
route: IRoute;
/** Перейти в раздел: меняет адрес (history.pushState) и перерисовывает приложение. */
navigate: (route: IRoute) => void;
/** Полный адрес раздела — для настоящих ссылок (Ctrl+клик, «открыть в новой вкладке»). */
buildHref: (route: IRoute) => string;
}
const RouterContext = React.createContext<IRouterValue | undefined>(undefined);
function readCurrentRoute(): IRoute {
return parseRoute(window.location.search);
}
function buildHref(route: IRoute): string {
return `${window.location.pathname}${buildSearch(route, window.location.search)}`;
}
export const RouterProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [route, setRoute] = React.useState<IRoute>(readCurrentRoute);
// Кнопки «назад/вперёд» браузера меняют адрес без нашего участия — перечитываем маршрут.
React.useEffect(() => {
const onPopState = (): void => setRoute(readCurrentRoute());
window.addEventListener('popstate', onPopState);
return () => window.removeEventListener('popstate', onPopState);
}, []);
const navigate = React.useCallback((next: IRoute): void => {
window.history.pushState(undefined, '', buildHref(next));
setRoute(next);
window.scrollTo(0, 0);
}, []);
const value = React.useMemo<IRouterValue>(() => ({ route, navigate, buildHref }), [route, navigate]);
return <RouterContext.Provider value={value}>{children}</RouterContext.Provider>;
};
export function useRouter(): IRouterValue {
const router = React.useContext(RouterContext);
if (!router) {
throw new Error('useRouter: компонент отрисован вне RouterProvider');
}
return router;
}