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,109 @@
|
||||
import { createT, formatAmd, isLocale, LOCALES, type Locale } from '@barev/i18n';
|
||||
import { Icon } from '@barev/ui';
|
||||
import type { Metadata } from 'next';
|
||||
import { getCatalog, type LandingOffer } from '../lib/data';
|
||||
import { BotLink } from './client';
|
||||
import { SERVICE_PAGES } from '../lib/services';
|
||||
import { FooterContacts, LegalLine } from './seo';
|
||||
|
||||
/** Главная (/) и «Уже живу» (/live): индексируются, у каждой свой canonical и hreflang */
|
||||
export async function homeMetadata(params: Promise<{ locale: string }>, path: '' | '/live'): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
if (!isLocale(locale)) return {};
|
||||
const t = createT(locale);
|
||||
const { offer } = await getCatalog();
|
||||
const title = t(path ? 'landing.live.metaTitle' : 'landing.home.metaTitle');
|
||||
const description = t(path ? 'landing.live.metaDescription' : 'landing.home.metaDescription', { pct: offer.eliteMaxPct });
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
alternates: {
|
||||
canonical: `/${locale}${path}`,
|
||||
languages: Object.fromEntries([...LOCALES.map((l) => [l, `/${l}${path}`]), ['x-default', `/ru${path}`]]),
|
||||
},
|
||||
openGraph: { title, description, locale, type: 'website', siteName: 'Barev Club' },
|
||||
};
|
||||
}
|
||||
|
||||
/** Переключатель аудитории: две версии главной — «Переезжаю» и «Уже живу в Армении» */
|
||||
export function AudienceSwitch({ locale, current }: { locale: Locale; current: 'move' | 'live' }) {
|
||||
const t = createT(locale);
|
||||
return (
|
||||
<nav className="h-switch" aria-label={t('landing.home.switchLabel')}>
|
||||
<a href={`/${locale}`} aria-current={current === 'move' ? 'page' : undefined}>
|
||||
{t('landing.home.switchMove')}
|
||||
</a>
|
||||
<a href={`/${locale}/live`} aria-current={current === 'live' ? 'page' : undefined}>
|
||||
{t('landing.home.switchLive')}
|
||||
</a>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Тарифы как клубные карты: Barev — прозрачная, Barev+ — розовая, Elite — жёлтая.
|
||||
* Оформление подписки — в боте, поэтому все кнопки ведут туда.
|
||||
*/
|
||||
export function ClubPlans({ locale, plusPrice, offer }: { locale: Locale; plusPrice: number; offer: LandingOffer }) {
|
||||
const t = createT(locale);
|
||||
const amd = (n: number) => formatAmd(locale, n);
|
||||
const plans = [
|
||||
{ id: 'free', name: 'Barev', price: t('landing.club.free'), cta: t('landing.club.freeCta') },
|
||||
{ id: 'plus', name: 'Barev+', price: t('landing.club.perMonth', { price: amd(plusPrice) }), cta: t('landing.club.plusCta') },
|
||||
{ id: 'elite', name: 'Barev Elite', price: t('landing.club.perMonth', { price: amd(offer.elitePrice) }), cta: t('landing.club.eliteCta') },
|
||||
] as const;
|
||||
return (
|
||||
<section id="plans" className="h-plans" aria-labelledby="plans-title">
|
||||
<div className="h-section-head">
|
||||
<h2 id="plans-title">{t('landing.club.title')}</h2>
|
||||
<p>{t('landing.club.sub')}</p>
|
||||
</div>
|
||||
<div className="h-plans__grid">
|
||||
{plans.map((p) => (
|
||||
<article key={p.id} className="h-plan" data-plan={p.id}>
|
||||
<div className="h-card">
|
||||
<span className="h-card__club">Barev Club</span>
|
||||
<span className="h-card__chip" aria-hidden="true" />
|
||||
<h3 className="h-card__name">{p.name}</h3>
|
||||
<span className="h-card__price">{p.price}</span>
|
||||
</div>
|
||||
<ul className="h-plan__list">
|
||||
{[1, 2, 3].map((n) => (
|
||||
<li key={n}>
|
||||
<Icon name="check" size={18} strokeWidth={2.6} />
|
||||
{t(`landing.club.${p.id}F${n}`, { pct: p.id === 'elite' ? offer.eliteMaxPct : offer.plusMaxPct })}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<BotLink className="bc-btn h-plan__cta">{p.cta}</BotLink>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function HomeFooter({ locale }: { locale: Locale }) {
|
||||
const t = createT(locale);
|
||||
return (
|
||||
<footer className="v-footer v-footer--dark">
|
||||
<nav className="h-footer__services" aria-label={t('landing.footer.services')}>
|
||||
{Object.values(SERVICE_PAGES).map((p) => (
|
||||
<a key={p.id} href={`/${locale}${p.path}`}>
|
||||
{t(`landing.svc.${p.id}.crumb`)}
|
||||
</a>
|
||||
))}
|
||||
<a href={`/${locale}/clean`}>{t('landing.svc.common.clean')}</a>
|
||||
</nav>
|
||||
<span className="h-footer__brand">
|
||||
{t('landing.footer.brand')}
|
||||
<LegalLine locale={locale} className="h-footer__legal" />
|
||||
</span>
|
||||
<span className="h-footer__links">
|
||||
<FooterContacts />
|
||||
<BotLink>{t('landing.footer.partner')}</BotLink>
|
||||
<a href={`/${locale}/privacy`}>{t('landing.footer.privacy')}</a>
|
||||
</span>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user