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,143 @@
|
||||
import '../../variants.css';
|
||||
import { quote } from '@barev/core';
|
||||
import { createT, formatAmd, isLocale, LOCALES, type Locale } from '@barev/i18n';
|
||||
import { CATEGORY_ICON, Icon, type CategoryCode } from '@barev/ui';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { BotLink, BotLinkProvider } from '../../../components/client';
|
||||
import { VariantHeader, variantMetadata } from '../../../components/variant';
|
||||
import { getCatalog, getCleaning } from '../../../lib/data';
|
||||
|
||||
export const revalidate = 300;
|
||||
export const dynamicParams = false;
|
||||
export function generateStaticParams() {
|
||||
return LOCALES.map((locale) => ({ locale }));
|
||||
}
|
||||
export function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
|
||||
return variantMetadata(params, '/land1', 'landing.v1.title');
|
||||
}
|
||||
|
||||
/** Версия 1 «Чат»: продукт живёт в Telegram, поэтому страница — переписка с ботом, кнопки — как inline-клавиатура */
|
||||
export default async function Land1({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale: raw } = await params;
|
||||
if (!isLocale(raw)) notFound();
|
||||
const locale: Locale = raw;
|
||||
const t = createT(locale);
|
||||
const amd = (n: number) => formatAmd(locale, n);
|
||||
const [catalog, cleaning] = await Promise.all([getCatalog(), getCleaning()]);
|
||||
// цена в диалоге — настоящая: обычная уборка, 2 комнаты, 1 санузел
|
||||
const demoPrice = quote(cleaning.pricing, { cleaningType: 'regular', rooms: 2, bathrooms: 1 }).totalPrice;
|
||||
const groups = catalog.categories.filter((c) => c.code !== 'PERKS' && c.services.length > 0);
|
||||
|
||||
const chat = [
|
||||
{ from: 'me', text: t('landing.v1.u1'), time: '10:02' },
|
||||
{ from: 'bot', text: t('landing.v1.b1', { price: amd(demoPrice) }), time: '10:02' },
|
||||
{ from: 'me', text: t('landing.v1.u2'), time: '10:03' },
|
||||
{ from: 'bot', text: t('landing.v1.b2'), time: '10:03' },
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<BotLinkProvider locale={locale} bot={catalog.botUsername}>
|
||||
<div className="v1">
|
||||
<div className="v-wrap">
|
||||
<VariantHeader locale={locale} path="/land1" />
|
||||
|
||||
<section className="v1-hero">
|
||||
<div className="v1-hero__text">
|
||||
<h1 className="v1-title">{t('landing.v1.title')}</h1>
|
||||
<p className="v1-sub">{t('landing.v1.sub')}</p>
|
||||
</div>
|
||||
|
||||
<div className="v1-chat" aria-label={t('landing.v1.botName')}>
|
||||
<div className="v1-chat__head">
|
||||
<span className="v1-avatar" aria-hidden="true">
|
||||
B
|
||||
</span>
|
||||
<span>
|
||||
<b>{t('landing.v1.botName')}</b>
|
||||
<small>{t('landing.v1.botStatus')}</small>
|
||||
</span>
|
||||
</div>
|
||||
<ol className="v1-chat__log">
|
||||
{chat.map((m, i) => (
|
||||
<li key={i} className={`v1-msg v1-msg--${m.from}`} style={{ ['--d' as string]: `${0.3 + i * 0.9}s` }}>
|
||||
{m.text}
|
||||
<time>{m.time}</time>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
{/* как inline-клавиатура под последним сообщением бота */}
|
||||
<div className="v1-keys" style={{ ['--d' as string]: `${0.3 + chat.length * 0.9}s` }}>
|
||||
<a href={`/${locale}/clean`} className="v1-key v1-key--main">
|
||||
{t('landing.hero.cleanCta')}
|
||||
</a>
|
||||
<BotLink className="v1-key">{t('landing.v1.cta')}</BotLink>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="v1-card">
|
||||
<h2 className="v1-h2">{t('landing.v1.canTitle')}</h2>
|
||||
<p className="v1-muted">{t('landing.v1.canText')}</p>
|
||||
<div className="v1-groups">
|
||||
{groups.map((g) => (
|
||||
<div key={g.code} className="v1-group">
|
||||
<h3>
|
||||
<Icon name={CATEGORY_ICON[g.code as CategoryCode]} size={20} />
|
||||
{t(`categories.${g.code}.name`)}
|
||||
</h3>
|
||||
<div className="v1-group__keys">
|
||||
{g.services.map((s) =>
|
||||
s.code === 'cleaning' && s.active ? (
|
||||
<a key={s.code} href={`/${locale}/clean`} className="v1-svc v1-svc--live">
|
||||
<span>{t(`services.${s.code}.name`)}</span>
|
||||
<span>{t('common.from', { price: amd(s.fromPrice) })}</span>
|
||||
</a>
|
||||
) : (
|
||||
<div key={s.code} className="v1-svc">
|
||||
<span>{t(`services.${s.code}.name`)}</span>
|
||||
<span>{s.active ? t('common.from', { price: amd(s.fromPrice) }) : t('common.soon')}</span>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="v1-why">
|
||||
<h2 className="v1-h2">{t('landing.v1.whyTitle')}</h2>
|
||||
<div className="v1-why__grid">
|
||||
{([1, 2, 3] as const).map((n) => (
|
||||
<div key={n} className="v1-why__item">
|
||||
<b>{t(`landing.v1.why${n}`)}</b>
|
||||
<span>{t(`landing.v1.why${n}Sub`)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="v1-final">
|
||||
<div>
|
||||
<h2 className="v1-final__title">{t('landing.v1.ctaTitle')}</h2>
|
||||
<p>{t('landing.v1.ctaText')}</p>
|
||||
</div>
|
||||
{/* поле ввода сообщения: «Barev!» уже набрано, кнопка отправки открывает бота */}
|
||||
<BotLink className="v1-compose">
|
||||
<span className="v1-compose__text">Barev!</span>
|
||||
<span className="v1-compose__send" aria-hidden="true">
|
||||
<Icon name="arrow" size={20} strokeWidth={2.4} />
|
||||
</span>
|
||||
<span className="v-sr">{t('landing.v1.cta')}</span>
|
||||
</BotLink>
|
||||
</section>
|
||||
|
||||
<footer className="v-footer">
|
||||
<span>{t('landing.footer.brand')}</span>
|
||||
<a href={`/${locale}/privacy`}>{t('landing.footer.privacy')}</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</BotLinkProvider>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user