Snapshot of uncommitted work: landing, bots, prod infra, partner cabinet, backoffice stage 1
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
45d7512d2b
commit
d452824c62
314 files changed
+44394
-121
No files matched your search
@@ -0,0 +1,149 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Center, Icon, Spinner } from '@barev/ui';
|
||||
import { I18nProvider, match, openTelegram, pickLocale, telegramLanguage, useI18n, useLocation, useTelegramBack, useTRPC } from '@barev/web';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Consent } from './screens/Consent.tsx';
|
||||
import { Docs } from './screens/Docs.tsx';
|
||||
import { Move } from './screens/Move.tsx';
|
||||
import { Segment } from './screens/Segment.tsx';
|
||||
import { ServiceRequest } from './screens/ServiceRequest.tsx';
|
||||
import { Home } from './screens/Home.tsx';
|
||||
import { Membership } from './screens/Membership.tsx';
|
||||
import { OrderDetail } from './screens/OrderDetail.tsx';
|
||||
import { OrderForm } from './screens/OrderForm.tsx';
|
||||
import { Orders } from './screens/Orders.tsx';
|
||||
import { Perks } from './screens/Perks.tsx';
|
||||
import { Profile } from './screens/Profile.tsx';
|
||||
import { Services } from './screens/Services.tsx';
|
||||
|
||||
const ROOTS = ['/', '/services', '/perks', '/profile', '/docs'];
|
||||
|
||||
export function App({ hasInitData }: { hasInitData: boolean }) {
|
||||
const trpc = useTRPC();
|
||||
const me = useQuery({ ...trpc.client.me.queryOptions(), enabled: hasInitData });
|
||||
const locale = pickLocale(me.data?.locale, telegramLanguage(), navigator.language);
|
||||
|
||||
return (
|
||||
<I18nProvider initial={locale}>
|
||||
{!hasInitData || me.error?.data?.httpStatus === 401 ? (
|
||||
<OpenInTelegram />
|
||||
) : me.isPending ? (
|
||||
<Center>
|
||||
<Spinner />
|
||||
</Center>
|
||||
) : me.error ? (
|
||||
<LoadError />
|
||||
) : !me.data.consented ? (
|
||||
<Consent />
|
||||
) : !me.data.segment ? (
|
||||
<Segment />
|
||||
) : (
|
||||
<Routes segment={me.data.segment} conciergeUrl={me.data.conciergeUrl} />
|
||||
)}
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Два сценария: MOVING — план переезда, документы, консьерж; LIVING — услуги со скидками, партнёры, заказы.
|
||||
* Остальные экраны (услуги, подписка, заказы) доступны из обоих.
|
||||
*/
|
||||
function Routes({ segment, conciergeUrl }: { segment: 'MOVING' | 'LIVING'; conciergeUrl: string }) {
|
||||
const { path } = useLocation();
|
||||
useTelegramBack(path, ROOTS);
|
||||
let screen: ReactNode;
|
||||
let m: Record<string, string> | null;
|
||||
if (path === '/') screen = segment === 'MOVING' ? <Move /> : <Home />;
|
||||
else if (path === '/docs') screen = <Docs />;
|
||||
else if ((m = match('/request/:code', path))) screen = <ServiceRequest code={m.code!} />;
|
||||
else if (path === '/services') screen = <Services />;
|
||||
else if ((m = match('/order/:code', path))) screen = <OrderForm code={m.code!} />;
|
||||
else if (path === '/orders') screen = <Orders />;
|
||||
else if ((m = match('/orders/:id', path))) screen = <OrderDetail id={m.id!} />;
|
||||
else if (path === '/perks') screen = <Perks />;
|
||||
else if (path === '/profile') screen = <Profile />;
|
||||
else if (path === '/membership') screen = <Membership />;
|
||||
else screen = segment === 'MOVING' ? <Move /> : <Home />;
|
||||
const withNav = ROOTS.includes(path) || path === '/orders';
|
||||
return (
|
||||
<>
|
||||
{screen}
|
||||
{withNav && (segment === 'MOVING' ? <MoveNav path={path} conciergeUrl={conciergeUrl} /> : <BottomNav path={path} />)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** Меню переезда: План / Документы / Консьерж (чат с ботом) / Услуги / Профиль */
|
||||
function MoveNav({ path, conciergeUrl }: { path: string; conciergeUrl: string }) {
|
||||
const { t } = useI18n();
|
||||
const item = (href: string, icon: Parameters<typeof Icon>[0]['name'], label: string) => (
|
||||
<a href={`#${href}`} aria-current={path === href ? 'page' : undefined}>
|
||||
<Icon name={icon} size={22} />
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
return (
|
||||
<nav className="bc-bottomnav" aria-label={t('client.nav.label')}>
|
||||
{item('/', 'globe', t('client.nav.plan'))}
|
||||
{item('/docs', 'docs', t('client.nav.docs'))}
|
||||
<button type="button" className="bc-bottomnav__plus" onClick={() => openTelegram(conciergeUrl)} aria-label={t('client.concierge.title')}>
|
||||
<Icon name="chat" size={24} strokeWidth={2.4} />
|
||||
</button>
|
||||
{item('/services', 'grid', t('client.nav.services'))}
|
||||
{item('/profile', 'user', t('client.nav.profile'))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
/** ORD-1: Главная / Услуги / + / Скидки / Профиль */
|
||||
function BottomNav({ path }: { path: string }) {
|
||||
const { t } = useI18n();
|
||||
const item = (href: string, icon: Parameters<typeof Icon>[0]['name'], label: string) => (
|
||||
<a href={`#${href}`} aria-current={path === href ? 'page' : undefined}>
|
||||
<Icon name={icon} size={22} />
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
return (
|
||||
<nav className="bc-bottomnav" aria-label={t('client.nav.label')}>
|
||||
{item('/', 'home', t('client.nav.home'))}
|
||||
{item('/services', 'grid', t('client.nav.services'))}
|
||||
<a href="#/order/cleaning" className="bc-bottomnav__plus" aria-label={t('client.nav.newOrder')}>
|
||||
<Icon name="plus" size={26} strokeWidth={2.6} />
|
||||
</a>
|
||||
{item('/perks', 'gift', t('client.nav.perks'))}
|
||||
{item('/profile', 'user', t('client.nav.profile'))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
function OpenInTelegram() {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<div className="app app--plain">
|
||||
<Center>
|
||||
<div className="bc-stack" style={{ textAlign: 'center', alignItems: 'center' }}>
|
||||
<Icon name="logo" size={48} style={{ color: 'var(--bc-plum)' }} />
|
||||
<h1 className="bc-h2">{t('client.openInTelegram.title')}</h1>
|
||||
<p className="bc-muted">{t('client.openInTelegram.text')}</p>
|
||||
</div>
|
||||
</Center>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LoadError() {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<div className="app app--plain">
|
||||
<Center>
|
||||
<div className="bc-stack" style={{ textAlign: 'center', alignItems: 'center' }}>
|
||||
<p>{t('errors.generic')}</p>
|
||||
<button className="bc-btn bc-btn--dark" onClick={() => window.location.reload()}>
|
||||
{t('common.retry')}
|
||||
</button>
|
||||
</div>
|
||||
</Center>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user