Apps, web, i18n, UI, partners, payouts, backoffice, bariki, prod infra
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
45d7512d2b
commit
3489e0b816
319 files changed
+45483
-121
No files matched your search
@@ -0,0 +1,214 @@
|
||||
import { createT, isLocale, type Locale } from '@barev/i18n';
|
||||
import type { Metadata } from 'next';
|
||||
import { getCatalog } from '../lib/data';
|
||||
import { languageAlternates, SITE } from '../lib/seo';
|
||||
import { range, SERVICE_PAGES, type ServicePageId } from '../lib/services';
|
||||
import { BotLink, BotLinkProvider } from './client';
|
||||
import { HomeFooter } from './home';
|
||||
import { RichText } from './rich-text';
|
||||
import { VariantHeader } from './variant';
|
||||
|
||||
/** Title, description, canonical и hreflang страницы услуги; цены в описании — из админки */
|
||||
export async function serviceMetadata(params: Promise<{ locale: string }>, id: ServicePageId): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
if (!isLocale(locale)) return {};
|
||||
const cfg = SERVICE_PAGES[id];
|
||||
const t = createT(locale);
|
||||
const vars = cfg.vars(await getCatalog(), locale);
|
||||
const k = `landing.svc.${id}`;
|
||||
const title = t(`${k}.meta.title`, vars);
|
||||
const description = t(`${k}.meta.description`, vars);
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
alternates: { canonical: `/${locale}${cfg.path}`, languages: languageAlternates(cfg.path) },
|
||||
openGraph: { title, description, locale, type: 'website', siteName: 'Barev Club' },
|
||||
twitter: { card: 'summary_large_image', title, description },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Страница услуги под поисковый запрос: прямой ответ с ценой в первом экране, факты, шаги,
|
||||
* подробности, цены и FAQ. Так её удобно читать людям и цитировать поиску и ИИ.
|
||||
*/
|
||||
export async function ServicePage({ locale, id }: { locale: Locale; id: ServicePageId }) {
|
||||
const cfg = SERVICE_PAGES[id];
|
||||
const t = createT(locale);
|
||||
const catalog = await getCatalog();
|
||||
const vars = cfg.vars(catalog, locale);
|
||||
const k = `landing.svc.${id}`;
|
||||
const c = (key: string) => t(`landing.svc.common.${key}`);
|
||||
const tv = (key: string) => t(`${k}.${key}`, vars);
|
||||
const others = Object.values(SERVICE_PAGES).filter((p) => p.id !== id);
|
||||
const url = `${SITE}/${locale}${cfg.path}`;
|
||||
|
||||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': [
|
||||
{
|
||||
'@type': 'Service',
|
||||
'@id': `${url}#service`,
|
||||
name: tv('h1'),
|
||||
serviceType: cfg.serviceType,
|
||||
description: tv('lead'),
|
||||
url,
|
||||
inLanguage: locale,
|
||||
provider: { '@id': `${SITE}/#org` },
|
||||
areaServed: { '@type': 'City', name: 'Yerevan' },
|
||||
offers: cfg.offers(catalog).map((o) => ({
|
||||
'@type': 'Offer',
|
||||
name: o.name,
|
||||
priceCurrency: 'AMD',
|
||||
...(o.minPrice ? { priceSpecification: { '@type': 'PriceSpecification', minPrice: o.price, priceCurrency: 'AMD' } } : { price: o.price }),
|
||||
url,
|
||||
})),
|
||||
},
|
||||
{
|
||||
'@type': 'FAQPage',
|
||||
'@id': `${url}#faq`,
|
||||
mainEntity: range(cfg.faq).map((n) => ({
|
||||
'@type': 'Question',
|
||||
name: tv(`faq.q${n}.q`),
|
||||
acceptedAnswer: { '@type': 'Answer', text: tv(`faq.q${n}.a`) },
|
||||
})),
|
||||
},
|
||||
{
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{ '@type': 'ListItem', position: 1, name: c('home'), item: `${SITE}/${locale}` },
|
||||
{ '@type': 'ListItem', position: 2, name: tv('crumb'), item: url },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<BotLinkProvider locale={locale} bot={catalog.botUsername}>
|
||||
<div className="v2">
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd).replace(/</g, '\\u003c') }} />
|
||||
<div className="v-wrap">
|
||||
<VariantHeader locale={locale} path={cfg.path} className="vh--dark" logoColor="#F6C3D3" />
|
||||
|
||||
<nav className="s-crumbs" aria-label={c('breadcrumbs')}>
|
||||
<a href={`/${locale}`}>{c('home')}</a>
|
||||
<span aria-hidden="true">/</span>
|
||||
<span aria-current="page">{tv('crumb')}</span>
|
||||
</nav>
|
||||
|
||||
<section className="s-hero">
|
||||
<div className="s-hero__text">
|
||||
<h1 className="s-title">{tv('h1')}</h1>
|
||||
<p className="s-lead">{tv('lead')}</p>
|
||||
<p className="s-updated">{c('updated')}</p>
|
||||
</div>
|
||||
<aside className="s-price" aria-label={tv('priceLabel')}>
|
||||
<span className="s-price__label">{tv('priceLabel')}</span>
|
||||
<span className="s-price__value">{tv('priceValue')}</span>
|
||||
<span className="s-price__note">{tv('priceNote')}</span>
|
||||
<BotLink className="bc-btn s-price__cta">{c('cta')}</BotLink>
|
||||
<a href={`/${locale}`} className="s-price__more">
|
||||
{c('ctaMove')} →
|
||||
</a>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section className="s-facts" aria-label={c('factsTitle')}>
|
||||
<h2 className="v-sr">{c('factsTitle')}</h2>
|
||||
<dl>
|
||||
{range(4).map((n) => (
|
||||
<div key={n} className="s-fact">
|
||||
<dt>{tv(`facts.f${n}.label`)}</dt>
|
||||
<dd>{tv(`facts.f${n}.value`)}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section className="s-section" aria-labelledby="steps-title">
|
||||
<h2 id="steps-title" className="s-h2">
|
||||
{tv('stepsTitle')}
|
||||
</h2>
|
||||
<ol className="v2-line s-steps">
|
||||
{range(cfg.steps).map((n) => (
|
||||
<li key={n} className="v2-step" data-live={n === 1}>
|
||||
<div className="v2-step__when">{tv(`steps.s${n}.when`)}</div>
|
||||
<div className="v2-step__body">
|
||||
<h3>{tv(`steps.s${n}.title`)}</h3>
|
||||
<p>{tv(`steps.s${n}.text`)}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<div className="s-body">
|
||||
{range(cfg.blocks).map((n) => (
|
||||
<section key={n} className="s-block">
|
||||
<h2 className="s-h2">{tv(`blocks.b${n}.title`)}</h2>
|
||||
<RichText text={tv(`blocks.b${n}.body`)} />
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<section className="s-section" aria-labelledby="prices-title">
|
||||
<h2 id="prices-title" className="s-h2">
|
||||
{c('pricesTitle')}
|
||||
</h2>
|
||||
<ul className="h-price__list">
|
||||
{range(cfg.prices).map((n) => (
|
||||
<li key={n} className="h-price__row">
|
||||
<b className="h-price__name">{tv(`prices.p${n}.name`)}</b>
|
||||
<span className="h-price__dots" aria-hidden="true" />
|
||||
<span className="h-price__value">{tv(`prices.p${n}.value`)}</span>
|
||||
<small className="h-price__hint">{tv(`prices.p${n}.note`)}</small>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="s-section" aria-labelledby="faq-title">
|
||||
<h2 id="faq-title" className="s-h2">
|
||||
{c('faqTitle')}
|
||||
</h2>
|
||||
<div className="s-faq">
|
||||
{range(cfg.faq).map((n) => (
|
||||
<details key={n} open={n === 1}>
|
||||
<summary>
|
||||
<h3>{tv(`faq.q${n}.q`)}</h3>
|
||||
</summary>
|
||||
<p>{tv(`faq.q${n}.a`)}</p>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="v2-final">
|
||||
<div>
|
||||
<h2>{tv('finalTitle')}</h2>
|
||||
<p>{tv('finalText')}</p>
|
||||
</div>
|
||||
<BotLink className="bc-btn v2-final__cta">{c('cta')}</BotLink>
|
||||
</section>
|
||||
|
||||
<section className="s-meta">
|
||||
<div>
|
||||
<h2 className="s-h3">{c('relatedTitle')}</h2>
|
||||
<ul className="s-related">
|
||||
{others.map((p) => (
|
||||
<li key={p.id}>
|
||||
<a href={`/${locale}${p.path}`}>{t(`landing.svc.${p.id}.crumb`)}</a>
|
||||
</li>
|
||||
))}
|
||||
<li>
|
||||
<a href={`/${locale}/clean`}>{c('clean')}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<HomeFooter locale={locale} />
|
||||
</div>
|
||||
</div>
|
||||
</BotLinkProvider>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user