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:
zabudko-ruandClaude Opus 5.5 committed 2026-10-25 22:55:29 +04:00
1 parent 45d7512d2b
commit 3489e0b816
319 files changed
+45483 -121

No files matched your search

@@ -0,0 +1,8 @@
import { redirect } from 'next/navigation';
/** /apartments → /ru/apartments (страница квартир есть только на русском); ?a= и UTM сохраняются */
export default async function ApartmentsRedirect({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {
const qs = new URLSearchParams();
for (const [k, v] of Object.entries(await searchParams)) if (typeof v === 'string') qs.set(k, v);
redirect(`/ru/apartments${qs.size ? `?${qs}` : ''}`);
}
@@ -0,0 +1,6 @@
import { redirectToLocale } from '../../../lib/redirect';
/** barevclub.com/clean → /<язык>/clean — короткий адрес для рекламы и QR */
export default async function CleanRedirect({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {
return redirectToLocale(searchParams, '/clean');
}
@@ -0,0 +1,8 @@
import { redirect } from 'next/navigation';
/** /flower → /ru/flower (страница цветов пока только на русском); ?a= и UTM сохраняются */
export default async function FlowerRedirect({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {
const qs = new URLSearchParams();
for (const [k, v] of Object.entries(await searchParams)) if (typeof v === 'string') qs.set(k, v);
redirect(`/ru/flower${qs.size ? `?${qs}` : ''}`);
}
@@ -0,0 +1,6 @@
import { redirectToLocale } from '../../../lib/redirect';
/** /land1 → /<язык>/land1 */
export default async function Land1Redirect({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {
return redirectToLocale(searchParams, '/land1');
}
@@ -0,0 +1,6 @@
import { redirectToLocale } from '../../../lib/redirect';
/** /land2 → /<язык>/land2 */
export default async function Land2Redirect({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {
return redirectToLocale(searchParams, '/land2');
}
@@ -0,0 +1,6 @@
import { redirectToLocale } from '../../../lib/redirect';
/** /land3 → /<язык>/land3 */
export default async function Land3Redirect({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {
return redirectToLocale(searchParams, '/land3');
}
@@ -0,0 +1,6 @@
import { redirectToLocale } from '../../../lib/redirect';
/** /land4 → /<язык>/land4 */
export default async function Land4Redirect({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {
return redirectToLocale(searchParams, '/land4');
}
@@ -0,0 +1,6 @@
import { redirectToLocale } from '../../../lib/redirect';
/** /land5 → /<язык>/land5 */
export default async function Land5Redirect({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {
return redirectToLocale(searchParams, '/land5');
}
@@ -0,0 +1,6 @@
import { redirectToLocale } from '../../../lib/redirect';
/** /land6 → /<язык>/land6 */
export default async function Land6Redirect({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {
return redirectToLocale(searchParams, '/land6');
}
+10
View File
@@ -0,0 +1,10 @@
import type { ReactNode } from 'react';
// Отдельный root layout для «/», который только перенаправляет на язык
export default function RedirectLayout({ children }: { children: ReactNode }) {
return (
<html lang="ru">
<body>{children}</body>
</html>
);
}
@@ -0,0 +1,6 @@
import { redirectToLocale } from '../../../lib/redirect';
/** /live → /<язык>/live */
export default async function LiveRedirect({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {
return redirectToLocale(searchParams, '/live');
}
+6
View File
@@ -0,0 +1,6 @@
import { redirectToLocale } from '../../lib/redirect';
/** «/» → /<язык> */
export default async function Root({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {
return redirectToLocale(searchParams);
}
@@ -0,0 +1,14 @@
import { createT, isLocale, LOCALES } from '@barev/i18n';
import { OG_SIZE, ogImage } from '../../../lib/og';
export const size = OG_SIZE;
export const contentType = 'image/png';
export function generateStaticParams() {
return LOCALES.map((locale) => ({ locale }));
}
/** OG-картинка страницы /airport — заголовок H1 */
export default async function OgImage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
return ogImage(createT(isLocale(locale) ? locale : 'ru')('landing.svc.airport.h1'));
}
@@ -0,0 +1,24 @@
import '../../variants.css';
import '../../home.css';
import '../../service.css';
import { isLocale, LOCALES } from '@barev/i18n';
import type { Viewport } from 'next';
import { notFound } from 'next/navigation';
import { ServicePage, serviceMetadata } from '../../../components/service-page';
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 serviceMetadata(params, 'airport');
}
export const viewport: Viewport = { themeColor: '#4A1028' };
/** /airport — страница услуги под поисковый запрос; тексты landing.svc.airport, цены из админки */
export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
if (!isLocale(locale)) notFound();
return <ServicePage locale={locale} id="airport" />;
}
@@ -0,0 +1,76 @@
import '../../variants.css';
import '../../home.css';
import '../../service.css';
import '../../flats.css';
import type { Metadata, Viewport } from 'next';
import { notFound } from 'next/navigation';
import { Flats } from '../../../components/flats';
import { HomeFooter } from '../../../components/home';
import { VariantHeader } from '../../../components/variant';
import { getCatalog } from '../../../lib/data';
import { getFlats } from '../../../lib/flats';
// Квартиры из CRM меняются в течение дня — страница собирается на каждый запрос, список кешируется на минуту.
export const dynamic = 'force-dynamic';
export const dynamicParams = false;
/** Только русская версия (решение владельца, 20.10.2026) */
export function generateStaticParams() {
return [{ locale: 'ru' }];
}
export const metadata: Metadata = {
title: 'Квартиры в аренду в Ереване — запись на просмотр | Barev Club',
description: 'Проверенные квартиры от агентства-партнёра: фото, описание, цена. Выберите квартиру и удобный день — агент позвонит и покажет её.',
alternates: { canonical: '/ru/apartments' },
openGraph: {
title: 'Квартиры в аренду в Ереване',
locale: 'ru',
type: 'website',
siteName: 'Barev Club',
},
};
export const viewport: Viewport = { themeColor: '#4A1028' };
/** /ru/apartments — «Найдём квартиру»: объекты AREV из CRM и запись на просмотр */
export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
if (locale !== 'ru') notFound();
const [flats, catalog] = await Promise.all([getFlats(), getCatalog()]);
const telegramUrl = `https://t.me/${catalog.botUsername}`;
return (
<div className="v2">
<div className="v-wrap">
<VariantHeader locale="ru" path="" className="vh--dark" logoColor="#F6C3D3" />
<nav className="s-crumbs" aria-label="Навигация">
<a href="/ru">Главная</a>
<span aria-hidden="true">/</span>
<span aria-current="page">Квартиры</span>
</nav>
<section className="fl-hero">
<h1 className="s-title">Найдём квартиру</h1>
<p className="s-lead">
Квартиры, которые агент уже проверил и согласовал с собственником. Листайте фото, читайте описание и выберите день — агент позвонит, подтвердит
время и покажет квартиру.
</p>
</section>
{flats?.length ? (
<Flats flats={flats} telegramUrl={telegramUrl} />
) : (
<section className="fl-empty">
<h2>{flats ? 'Сейчас свободных квартир нет' : 'Список квартир временно недоступен'}</h2>
<p>Напишите нам в Telegram — подберём варианты под ваш бюджет и район.</p>
<a className="bc-btn fl-book" href={telegramUrl}>
Написать в Telegram
</a>
</section>
)}
<HomeFooter locale="ru" />
</div>
</div>
);
}
@@ -0,0 +1,15 @@
import { createT, isLocale, LOCALES } from '@barev/i18n';
import { OG_SIZE, ogImage } from '../../../lib/og';
export const size = OG_SIZE;
export const contentType = 'image/png';
export function generateStaticParams() {
return LOCALES.map((locale) => ({ locale }));
}
/** OG-картинка страницы заказа уборки */
export default async function OgImage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const t = createT(isLocale(locale) ? locale : 'ru');
return ogImage(t('landing.clean.meta.title').replace(/ — Barev Club$/, ''));
}
+123
View File
@@ -0,0 +1,123 @@
import '../../clean.css';
import { createT, isLocale, LOCALES, pickMessages, type Locale } from '@barev/i18n';
import { Logo } from '@barev/ui';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { CleanForm } from '../../../components/clean';
import { FooterContacts, LegalLine } from '../../../components/seo';
import { LegalLinks } from '../../../components/legal';
import { BotLink, BotLinkProvider, LangMenu, LangSwitch } from '../../../components/client';
import { getCleaning } from '../../../lib/data';
export const revalidate = 300;
export const dynamicParams = false;
export function generateStaticParams() {
return LOCALES.map((locale) => ({ locale }));
}
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
const { locale } = await params;
if (!isLocale(locale)) return {};
const t = createT(locale);
return {
title: t('landing.clean.meta.title'),
description: t('landing.clean.meta.description'),
alternates: {
canonical: `/${locale}/clean`,
languages: Object.fromEntries([...LOCALES.map((l) => [l, `/${l}/clean`]), ['x-default', '/ru/clean']]),
},
openGraph: { title: t('landing.clean.meta.title'), description: t('landing.clean.meta.description'), locale, type: 'website', siteName: 'Barev Club' },
};
}
const ZONES = ['kitchen', 'bathroom', 'rooms'] as const;
/** barevclub.com/clean — заказ уборки с сайта по модели qlean.ru */
export default async function CleanPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale: raw } = await params;
if (!isLocale(raw)) notFound();
const locale: Locale = raw;
const t = createT(locale);
const config = await getCleaning();
const messages = pickMessages(locale, ['landing.clean.', 'params.cleaning.', 'extras.', 'districts.', 'errors.', 'common.']);
return (
<BotLinkProvider locale={locale} bot={config.botUsername}>
<div className="wrap cl-wrap">
<header className="header">
<a href={`/${locale}`} aria-label={t('landing.clean.home')} style={{ display: 'inline-flex' }}>
<Logo />
</a>
<nav className="nav" aria-label={t('landing.nav.label')}>
<a href={`/${locale}`}>{t('landing.clean.home')}</a>
<a href="#included">{t('landing.clean.includedTitle')}</a>
<a href="#how">{t('landing.clean.howTitle')}</a>
</nav>
<div className="langs">
<span className="langs__row">
<LangSwitch current={locale} path="/clean" />
</span>
<LangMenu current={locale} path="/clean" />
<BotLink className="bc-btn bc-btn--dark bc-btn--sm" style={{ marginLeft: 6 }}>
<span className="tg-long">{t('landing.openTelegram')}</span>
<span className="tg-short">Telegram</span>
</BotLink>
</div>
</header>
<CleanForm locale={locale} config={config} messages={messages} />
<section id="included" className="cl-included">
<h2 className="cl-section-title">{t('landing.clean.includedTitle')}</h2>
<div className="cl-zones">
{ZONES.map((z) => (
<div key={z} className="cl-zone">
<h3>{t(`landing.clean.zone.${z}`)}</h3>
<ul>
{t(`landing.clean.inc.${z}`)
.split('\n')
.map((line) => (
<li key={line}>{line}</li>
))}
</ul>
</div>
))}
</div>
<div className="cl-variants">
<p>{t('landing.clean.general')}</p>
<p>{t('landing.clean.renovation')}</p>
</div>
</section>
<section id="how" className="how cl-how">
<h2 className="bc-h2" style={{ fontSize: 34 }}>
{t('landing.clean.howTitle')}
</h2>
<ol className="cl-how__grid">
{([1, 2, 3] as const).map((n) => (
<li key={n} className="how__step">
<div className="how__num" style={{ background: n === 1 ? 'var(--bc-home)' : n === 2 ? 'var(--bc-blue)' : 'var(--bc-pink)', color: 'var(--bc-ink)' }}>
{n}
</div>
<div style={{ fontWeight: 600 }}>{t(`landing.clean.how${n}`)}</div>
<div style={{ fontSize: 13, color: '#CFC8C2' }}>{t(`landing.clean.how${n}Sub`)}</div>
</li>
))}
</ol>
</section>
<footer className="footer">
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<span style={{ fontFamily: 'var(--bc-font-head)', fontWeight: 700, color: 'var(--bc-plum)' }}>{t('landing.footer.brand')}</span>
<LegalLine locale={locale} />
</div>
<div className="footer__links">
<a href={`/${locale}`}>{t('landing.clean.home')}</a>
<FooterContacts />
<LegalLinks locale={locale} />
</div>
</footer>
</div>
</BotLinkProvider>
);
}
+186
View File
@@ -0,0 +1,186 @@
import '../../variants.css';
import '../../home.css';
import '../../service.css';
import '../../flower.css';
import { Flower } from '@barev/ui';
import type { Metadata, Viewport } from 'next';
import { notFound } from 'next/navigation';
import { BotLink, BotLinkProvider } from '../../../components/client';
import { FlowerPlans, NextDelivery } from '../../../components/flowers';
import { HomeFooter } from '../../../components/home';
import { VariantHeader } from '../../../components/variant';
import { getCatalog } from '../../../lib/data';
export const revalidate = 300;
export const dynamicParams = false;
/** Пока только русская версия — как /apartments */
export function generateStaticParams() {
return [{ locale: 'ru' }];
}
export const metadata: Metadata = {
title: 'Цветы по подписке в Ереване — свежий букет каждую неделю | Barev Club',
description: 'Сезонные букеты S, M и L по подписке: раз в неделю, раз в 2 недели или раз в месяц. Цветы из армянских теплиц, доставка по Еревану бесплатно, пауза в один клик.',
alternates: { canonical: '/ru/flower' },
openGraph: { title: 'Цветы по подписке в Ереване', locale: 'ru', type: 'website', siteName: 'Barev Club' },
};
export const viewport: Viewport = { themeColor: '#4A1028' };
const STEPS = [
{ when: 'Сегодня', title: 'Выберите размер и частоту', text: 'S, M или L — раз в неделю, раз в 2 недели или раз в месяц. Пожелания вроде «без лилий» или «светлые тона» напишите в чат.' },
{ when: 'За день', title: 'Флорист соберёт букет', text: 'Из того, что сейчас самое свежее: цветы приезжают с теплиц Гохта и Армавира за 1–2 дня до вас. Каждый раз новый букет.' },
{ when: 'В ваш день', title: 'Привезём домой или в офис', text: 'Утром, 9–13, или вечером, 17–21. Курьер позвонит за 30 минут. Доставка по Еревану бесплатно.' },
{ when: 'Когда угодно', title: 'Пауза или отмена в один клик', text: 'Уезжаете — поставьте подписку на паузу в Telegram за 48 часов до доставки. Без звонков и штрафов.' },
];
const SEASONS = [
{ tone: 'learn', name: 'Весна', text: 'Тюльпаны, ранункулюсы, мимоза и гиацинты. К 8 марта и 7 апреля — особые букеты.' },
{ tone: 'pink', name: 'Начало лета', text: 'Пионы, пионовидные розы, альстромерия и полевые цветы.' },
{ tone: 'perks', name: 'Лето', text: 'Подсолнухи, эустома, гортензия и розы на пике сезона.' },
{ tone: 'docs', name: 'Осень и зима', text: 'Георгины, хризантемы, гиперикум, эвкалипт и тепличные розы.' },
] as const;
const FAQ = [
['Можно выбрать цветы самому?', 'Состав подбирает флорист из того, что сейчас в сезоне, — поэтому букеты свежее и каждый раз разные. Пожелания и аллергии учтём.'],
['Когда списываются деньги?', 'За 2 дня до каждой доставки. Предоплаты за месяц нет. Пакеты на 3, 6 и 12 месяцев оплачиваются сразу.'],
['Как поставить на паузу или отменить?', 'В Telegram, не позже чем за 48 часов до доставки. Пауза — на любой срок, отмена — без штрафов.'],
['Куда вы доставляете?', 'По всему Еревану бесплатно. В Абовян, Эчмиадзин и Масис — за 2 000 ֏.'],
['Что, если меня не будет дома?', 'Курьер позвонит за 30 минут. Оставим букет консьержу или соседу либо перенесём доставку на следующий день.'],
['Зимой букеты будут такими же?', 'Размер и количество стеблей не меняются. Зимой в букетах больше тепличных роз, хризантем и эвкалипта.'],
] as const;
/** /ru/flower — цветы по подписке: тарифы S/M/L, оформление в Telegram */
export default async function FlowerPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
if (locale !== 'ru') notFound();
const catalog = await getCatalog();
return (
<BotLinkProvider locale="ru" bot={catalog.botUsername}>
<div className="v2">
<div className="v-wrap">
<VariantHeader locale="ru" path="" className="vh--dark" logoColor="#F6C3D3" />
<nav className="s-crumbs" aria-label="Навигация">
<a href="/ru">Главная</a>
<span aria-hidden="true">/</span>
<span aria-current="page">Цветы</span>
</nav>
<section className="v2-hero fw-hero">
<div className="v2-word fw-word" aria-hidden="true">
Цветы
<span className="fw-word__sticker">
<Flower size={112} color="#F7DA6E" textColor="#4A1028" rotate={-12} fontSize={17}>
{'от\n8 900 ֏'}
</Flower>
</span>
</div>
<ul className="v2-hello" aria-hidden="true">
<li>каждую неделю</li>
<li>раз в 2 недели</li>
<li>раз в месяц</li>
</ul>
<div className="v2-hero__text">
<h1 className="v2-title">Свежие цветы дома — по подписке</h1>
<div className="h-hero__side">
<p className="v2-sub">Флорист соберёт сезонный букет, а мы привезём его в удобный день. Вам останется поставить цветы в вазу.</p>
<div className="h-actions">
<a href="#plans" className="bc-btn h-cta">
Выбрать букет
</a>
<BotLink className="bc-btn h-cta-ghost">Написать в Telegram</BotLink>
</div>
<NextDelivery />
</div>
</div>
</section>
<ol className="v2-line" aria-label="Как это работает">
{STEPS.map((s, i) => (
<li key={s.when} className="v2-step" data-live={i === 0}>
<div className="v2-step__when">{s.when}</div>
<div className="v2-step__body">
<h2>{s.title}</h2>
<p>{s.text}</p>
</div>
<div className="v2-step__meta">{i === 2 && <span className="v2-chip">0 ֏ доставка</span>}</div>
</li>
))}
</ol>
<FlowerPlans />
<section className="fw-section" aria-labelledby="season-title">
<div className="h-section-head">
<h2 id="season-title">Что будет в букетах</h2>
<p>Берём то, что сейчас в сезоне в армянских теплицах, поэтому цветы свежее и стоят дольше.</p>
</div>
<div className="fw-seasons">
{SEASONS.map((s) => (
<div key={s.name} className={`fw-season bc-tone-${s.tone}`}>
<h3>{s.name}</h3>
<p>{s.text}</p>
</div>
))}
</div>
</section>
<section className="fw-duo">
<div className="fw-gift">
<span className="fw-kicker">Для тех, кто далеко</span>
<h2>Цветы маме в Ереван — каждый месяц</h2>
<p>Оплачиваете из любой страны, мы вручаем букет с вашей открыткой и присылаем фото в WhatsApp или Telegram. Пакеты на 3, 6 и 12 месяцев.</p>
<div className="fw-gift__prices">
<div>
<b>$39</b>
<span>букет M</span>
</div>
<div>
<b>$59</b>
<span>букет L</span>
</div>
<div>
<b>$89</b>
<span>L + ваза и сладости</span>
</div>
</div>
<BotLink className="bc-btn fw-gift__cta">Подарить подписку</BotLink>
</div>
<div className="fw-guarantee">
<Flower size={84} color="#1F3311" textColor="#A8C38A" rotate={8} fontSize={15}>
100%
</Flower>
<h2>Первый букет без риска</h2>
<p>Не понравится — пришлите фото в течение суток, и мы вернём деньги полностью. Если букет простоял меньше 5 дней, заменим его бесплатно.</p>
</div>
</section>
<section className="fw-faq-wrap" aria-labelledby="faq-title">
<div className="h-section-head">
<h2 id="faq-title">Частые вопросы</h2>
</div>
<div className="faq fw-faq">
{FAQ.map(([q, a]) => (
<details key={q}>
<summary>{q}</summary>
<p>{a}</p>
</details>
))}
</div>
</section>
<section className="v2-final">
<div>
<h2>Первый букет — уже на этой неделе</h2>
<p>Напишите в Telegram размер и частоту — подтвердим день доставки и пришлём ссылку на оплату. Цена для первых 100 подписчиков сохранится, даже когда вырастет.</p>
</div>
<BotLink className="bc-btn v2-final__cta">Оформить подписку</BotLink>
</section>
<HomeFooter locale="ru" />
</div>
</div>
</BotLinkProvider>
);
}
+289
View File
@@ -0,0 +1,289 @@
import { createT, formatAmd, isLocale, LOCALES, type Locale } from '@barev/i18n';
import { CATEGORY_ICON, Flower, Icon, Logo, type CategoryCode } from '@barev/ui';
import { notFound } from 'next/navigation';
import { BotLink, BotLinkProvider, LangSwitch } from '../../../components/client';
import { getCatalog } from '../../../lib/data';
export const revalidate = 300;
export const dynamicParams = false;
export function generateStaticParams() {
return LOCALES.map((locale) => ({ locale }));
}
/** Прежняя главная (до октября 2026) — сохранена для сравнения, в поиск не попадает */
export const metadata = { robots: { index: false, follow: true } };
const TONE: Record<CategoryCode, { bg: string; ink: string }> = {
HOME: { bg: 'var(--bc-home)', ink: 'var(--bc-home-ink)' },
DOCS: { bg: 'var(--bc-docs)', ink: 'var(--bc-docs-ink)' },
LEARNING: { bg: 'var(--bc-learn)', ink: 'var(--bc-learn-ink)' },
PERKS: { bg: 'var(--bc-perks)', ink: 'var(--bc-perks-ink)' },
};
/** Прежняя главная: лендинг v2 · плотный (LND-1) */
export default async function Landing({ 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 = await getCatalog();
const cat = (code: CategoryCode) => catalog.categories.find((c) => c.code === code);
const cleaningFrom = cat('HOME')?.services.find((s) => s.code === 'cleaning')?.fromPrice ?? 0;
const maxDiscount = Math.max(0, ...catalog.categories.map((c) => c.barevPlusDiscountPct));
const discounts = catalog.categories.filter((c) => c.barevPlusDiscountPct > 0);
const preview = [
{ tone: 'var(--bc-blue)', ink: 'var(--bc-blue-ink)', title: t('landing.preview.cleaningRegular'), meta: t('landing.preview.cleaningRegularMeta'), badge: t('status.CLEANING.EN_ROUTE') },
{ tone: 'var(--bc-home)', ink: 'var(--bc-home-ink)', title: t('landing.preview.cleaning'), meta: t('landing.preview.cleaningMeta'), badge: t('status.CLEANING.ASSIGNED') },
{ tone: 'var(--bc-docs)', ink: 'var(--bc-docs-ink)', title: t('services.social_card.name'), meta: t('status.DOCUMENTS.DOCS_REVIEW'), badge: t('landing.preview.inWork') },
{ tone: 'var(--bc-learn)', ink: 'var(--bc-learn-ink)', title: t('landing.preview.lesson'), meta: t('landing.preview.lessonMeta'), badge: t('landing.preview.today') },
];
const faq = ['install', 'languages', 'plus', 'gift'] as const;
const check = (color: string) => <Icon name="check" size={18} strokeWidth={2.6} style={{ color }} />;
return (
<BotLinkProvider locale={locale} bot={catalog.botUsername}>
<div className="wrap">
<header className="header">
<Logo />
<nav className="nav" aria-label={t('landing.nav.label')}>
<a href="#services">{t('landing.nav.services')}</a>
<a href="#how">{t('landing.nav.how')}</a>
<a href="#plans">{t('landing.nav.plans')}</a>
<a href="#faq">{t('landing.nav.faq')}</a>
</nav>
<div className="langs">
<LangSwitch current={locale} path="/land0" />
<BotLink className="bc-btn bc-btn--dark bc-btn--sm" style={{ marginLeft: 6 }}>
{t('landing.openTelegram')}
</BotLink>
</div>
</header>
<section className="row">
<div className="hero">
<div className="hero__tag">{t('landing.hero.tag')}</div>
<h1 className="bc-h1">{t('landing.hero.title')}</h1>
<p className="hero__sub">{t('landing.hero.sub')}</p>
<div className="row" style={{ gap: 10 }}>
<BotLink className="bc-btn bc-btn--dark" style={{ height: 56, padding: '0 28px' }}>
{t('landing.hero.cta')}
<Icon name="arrow" size={18} strokeWidth={2.2} />
</BotLink>
<a href="#services" className="bc-btn bc-btn--light" style={{ height: 56 }}>
{t('landing.hero.services')}
</a>
</div>
{/* сейчас заказать можно уборку — короткий путь на форму /clean */}
<a href={`/${locale}/clean`} className="clean-promo">
<span className="clean-promo__text">
<b>{t('landing.hero.cleanTitle', { price: amd(cleaningFrom) })}</b>
<span>{t('landing.hero.cleanText')}</span>
</span>
<span className="bc-btn bc-btn--accent clean-promo__cta">{t('landing.hero.cleanCta')}</span>
</a>
</div>
<div className="side">
<div className="bc-card" style={{ borderRadius: 36, display: 'flex', flexDirection: 'column', gap: 8 }}>
<div className="bc-between" style={{ padding: '0 4px 6px' }}>
<div className="bc-h3" style={{ fontSize: 20 }}>
{t('landing.preview.title')}
</div>
<div className="bc-small bc-muted">{t('landing.preview.inApp')}</div>
</div>
{preview.map((p) => (
<div key={p.title} className="preview-row" style={{ background: p.tone, color: p.ink }}>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 600, fontSize: 14 }}>{p.title}</div>
<div style={{ fontSize: 12 }}>{p.meta}</div>
</div>
<span className="bc-badge">{p.badge}</span>
</div>
))}
</div>
<div className="row">
<div className="bc-card bc-tone-perks" style={{ flex: 1, borderRadius: 30, padding: 18, display: 'flex', alignItems: 'center', gap: 12 }}>
<Flower size={72} rotate={-10}>
5
</Flower>
<div className="langs-sticker">
<span>{t('landing.languages')}</span>
<span className="langs-sticker__list">RU EN 中文 HY ES</span>
</div>
</div>
<div className="bc-card bc-tone-learn" style={{ flex: 1, borderRadius: 30, padding: 18, display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 4 }}>
<div style={{ fontFamily: 'var(--bc-font-head)', fontWeight: 700, fontSize: 18 }}>{t('landing.noInstall')}</div>
<div className="bc-small">{t('landing.noInstallSub')}</div>
</div>
</div>
</div>
</section>
<section id="services" className="row">
{(['HOME', 'DOCS', 'LEARNING'] as const).map((code) => (
<div key={code} className="dir" style={{ background: TONE[code].bg, color: TONE[code].ink }}>
<div className="bc-between">
<h2 className="dir__title" style={{ margin: 0 }}>
{t(`categories.${code}.name`)}
</h2>
<Icon name={CATEGORY_ICON[code]} size={28} />
</div>
<div className="bc-stack" style={{ gap: 6 }}>
{(cat(code)?.services ?? []).map((s) =>
s.code === 'cleaning' && s.active ? (
<a key={s.code} href={`/${locale}/clean`} className="bc-row-pill">
<span style={{ fontWeight: 600 }}>{t(`services.${s.code}.name`)}</span>
<span>{t('common.from', { price: amd(s.fromPrice) })}</span>
</a>
) : (
<div key={s.code} className="bc-row-pill" data-soon={!s.active}>
<span style={{ fontWeight: 600 }}>{t(`services.${s.code}.name`)}</span>
<span>{s.active ? t('common.from', { price: amd(s.fromPrice) }) : t('common.soon')}</span>
</div>
),
)}
</div>
<div style={{ fontSize: 14 }}>{t(`landing.dir.${code}`)}</div>
{code === 'HOME' ? (
<a href={`/${locale}/clean`} style={{ marginTop: 'auto', fontWeight: 700, color: TONE[code].ink }}>
{t('landing.hero.cleanCta')} →
</a>
) : (
<BotLink style={{ marginTop: 'auto', fontWeight: 700, color: TONE[code].ink }}>
{(cat(code)?.services ?? []).some((s) => s.active) ? t(`landing.dir.${code}_cta`) : t('landing.dir.soonCta')} →
</BotLink>
)}
</div>
))}
<div className="dir" style={{ background: TONE.PERKS.bg, color: TONE.PERKS.ink }}>
<div className="bc-between">
<h2 className="dir__title" style={{ margin: 0 }}>
{t('categories.PERKS.name')}
</h2>
<Icon name="gift" size={28} />
</div>
<div className="perks-big">{t('landing.perks.upTo', { pct: maxDiscount })}</div>
<div className="bc-chips">
{(['cafes', 'restaurants', 'gifts'] as const).map((k) => (
<span key={k} className="bc-badge" style={{ padding: '8px 14px', fontSize: 14, fontWeight: 500 }}>
{t(`landing.perks.${k}`)}
</span>
))}
</div>
<div style={{ fontSize: 14 }}>{t('landing.perks.text')}</div>
<BotLink style={{ marginTop: 'auto', fontWeight: 700, color: TONE.PERKS.ink }}>{t('landing.perks.cta')} →</BotLink>
</div>
</section>
<section id="how" className="row">
<div className="how">
<h2 className="bc-h2" style={{ fontSize: 34 }}>
<span style={{ fontWeight: 700 }}>{t('landing.how.titleBold')}</span> <span className="bc-italic">{t('landing.how.titleItalic')}</span>
</h2>
<div className="how__grid">
{[
{ bg: 'var(--bc-pink)', ink: 'var(--bc-plum)' },
{ bg: 'var(--bc-perks)', ink: 'var(--bc-plum)' },
{ bg: 'var(--bc-home)', ink: 'var(--bc-home-ink)' },
{ bg: 'var(--bc-blue)', ink: 'var(--bc-blue-ink)' },
].map((c, i) => (
<div key={i} className="how__step">
<div className="how__num" style={{ background: c.bg, color: c.ink }}>
{i + 1}
</div>
<div style={{ fontWeight: 600 }}>{t(`landing.how.step${i + 1}`)}</div>
<div style={{ fontSize: 13, color: '#CFC8C2' }}>{t(`landing.how.step${i + 1}_sub`)}</div>
</div>
))}
</div>
</div>
<div className="agency">
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 8 }}>
<div style={{ fontFamily: 'var(--bc-font-head)', fontWeight: 800, fontSize: 23, lineHeight: 1.1, textWrap: 'balance', hyphens: 'manual' }}>{t('landing.agency.title')}</div>
<div style={{ fontSize: 15 }}>{t('landing.agency.text')}</div>
</div>
<div className="sticker">
<div style={{ fontFamily: 'var(--bc-font-head)', fontStyle: 'italic', fontSize: 20, fontWeight: 500 }}>{t('landing.agency.gift')}</div>
<div style={{ fontSize: 12 }}>{t('landing.agency.giftSub')}</div>
</div>
</div>
</section>
<section id="plans" className="row" style={{ alignItems: 'stretch' }}>
<div className="plan" style={{ background: '#fff' }}>
<div className="bc-between" style={{ alignItems: 'baseline' }}>
<div className="bc-h2" style={{ fontSize: 28 }}>
Barev
</div>
<div className="bc-num" style={{ fontSize: 32 }}>
{amd(0)}
</div>
</div>
<div className="plan__list">
<div>
{check('var(--bc-ok)')}
{t('plans.free.perks')}
</div>
<div>
{check('var(--bc-ok)')}
{t('plans.free.prices')}
</div>
<div>
{check('var(--bc-ok)')}
{t('plans.free.status')}
</div>
</div>
<BotLink className="bc-btn bc-btn--outline bc-btn--block" style={{ marginTop: 'auto' }}>
{t('landing.hero.cta')}
</BotLink>
</div>
<div className="plan bc-tone-learn">
<div className="bc-between" style={{ alignItems: 'baseline' }}>
<div className="bc-h2" style={{ fontSize: 28 }}>
Barev+
</div>
<div className="bc-num" style={{ fontSize: 24 }}>
{t('plans.plus.price', { price: amd(catalog.barevPlus.monthlyPrice) })}
</div>
</div>
<div className="plan__list">
<div>
{check('var(--bc-accent)')}
{t('plans.plus.all')}
</div>
{discounts.map((c) => (
<div key={c.code}>
{check('var(--bc-accent)')}
{t('plans.plus.discount', { pct: c.barevPlusDiscountPct, category: t(`categories.${c.code}.name`) })}
</div>
))}
</div>
<BotLink className="bc-btn bc-btn--dark bc-btn--block" style={{ marginTop: 'auto' }}>
{t('landing.plans.tryPlus')}
</BotLink>
</div>
<div id="faq" className="faq">
<h2 className="bc-h2" style={{ fontSize: 28, marginBottom: 4 }}>
{t('landing.nav.faq')}
</h2>
{faq.map((k, i) => (
<details key={k} open={i === 0}>
<summary>{t(`landing.faq.${k}.q`)}</summary>
<p>{t(`landing.faq.${k}.a`, { pct: maxDiscount, days: catalog.barevPlus.giftDays })}</p>
</details>
))}
</div>
</section>
<footer className="footer">
<div style={{ fontFamily: 'var(--bc-font-head)', fontWeight: 700, color: 'var(--bc-plum)' }}>{t('landing.footer.brand')}</div>
<div className="footer__links">
<BotLink>{t('landing.footer.partner')}</BotLink>
{process.env.CONTACT_EMAIL && <a href={`mailto:${process.env.CONTACT_EMAIL}`}>{process.env.CONTACT_EMAIL}</a>}
<a href={`/${locale}/privacy`}>{t('landing.footer.privacy')}</a>
</div>
</footer>
</div>
</BotLinkProvider>
);
}
+143
View File
@@ -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>
);
}
+11
View File
@@ -0,0 +1,11 @@
import { isLocale } from '@barev/i18n';
import { notFound, permanentRedirect } from 'next/navigation';
/** Версия 2 «Переезд по шагам» стала главной — старые ссылки /land2 ведут на неё с теми же параметрами */
export default async function Land2({ params, searchParams }: { params: Promise<{ locale: string }>; searchParams: Promise<Record<string, string | string[] | undefined>> }) {
const [{ locale }, sp] = await Promise.all([params, searchParams]);
if (!isLocale(locale)) notFound();
const qs = new URLSearchParams();
for (const [k, v] of Object.entries(sp)) if (typeof v === 'string') qs.set(k, v);
permanentRedirect(`/${locale}${qs.size ? `?${qs}` : ''}`);
}
+111
View File
@@ -0,0 +1,111 @@
import '../../variants.css';
import { createT, formatAmd, isLocale, LOCALES, type Locale } from '@barev/i18n';
import { Icon } from '@barev/ui';
import { notFound } from 'next/navigation';
import { BotLink, BotLinkProvider } from '../../../components/client';
import { GiftCode } from '../../../components/gift-code';
import { VariantHeader, variantMetadata } from '../../../components/variant';
import { getCatalog } 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, '/land3', 'landing.v3.title');
}
/** Версия 3 «Подарок от агентства»: для тех, кто пришёл по QR из договора аренды. Бирка для ключей с месяцем Barev+ */
export default async function Land3({ 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 = await getCatalog();
const home = catalog.categories.find((c) => c.code === 'HOME');
const cleaningFrom = home?.services.find((s) => s.code === 'cleaning')?.fromPrice ?? 0;
const homePct = home?.barevPlusDiscountPct ?? 0;
// так же округляет скидку сервер: вниз до целого драма
const cleaningWithPlus = cleaningFrom - Math.floor((cleaningFrom * homePct) / 100);
const discounts = catalog.categories.filter((c) => c.barevPlusDiscountPct > 0);
return (
<BotLinkProvider locale={locale} bot={catalog.botUsername}>
<div className="v3">
<div className="v-wrap">
<VariantHeader locale={locale} path="/land3" />
<section className="v3-hero">
<div className="v3-hero__text">
<span className="v3-kicker">{t('landing.v3.tag')}</span>
<h1 className="v3-title">{t('landing.v3.title')}</h1>
<p className="v3-sub">{t('landing.v3.sub')}</p>
<BotLink className="bc-btn bc-btn--accent v3-cta">{t('landing.v3.cta')}</BotLink>
</div>
<div className="v3-tagwrap" aria-hidden="true">
<svg className="v3-string" viewBox="0 0 120 160" fill="none">
<path d="M60 0 C 20 40, 100 80, 60 132" stroke="currentColor" strokeWidth="3" strokeLinecap="round" />
</svg>
<div className="v3-tag">
<span className="v3-tag__hole" />
<span className="v3-tag__label">{t('landing.agency.gift')}</span>
<span className="v3-tag__title">{t('landing.v3.giftTitle')}</span>
<GiftCode withCode={t('landing.v3.giftCode')} withoutCode={t('landing.v3.giftNoCode')} />
</div>
</div>
</section>
<section className="v3-steps">
<h2 className="v3-h2">{t('landing.v3.stepsTitle')}</h2>
<ol>
{([1, 2, 3] as const).map((n) => (
<li key={n}>
<span className="v3-num">{n}</span>
<b>{t(`landing.v3.step${n}`)}</b>
<span>{t(`landing.v3.step${n}Sub`)}</span>
</li>
))}
</ol>
</section>
<section className="v3-benefits">
<div className="v3-deal">
<h2 className="v3-h2">{t('landing.v3.benefitsTitle')}</h2>
{homePct > 0 && cleaningFrom > 0 && (
<a href={`/${locale}/clean`} className="v3-deal__card">
<b>{t('landing.v3.cleaningDeal', { price: amd(cleaningWithPlus), full: amd(cleaningFrom) })}</b>
<span>{t('landing.v3.cleaningDealSub')}</span>
<span className="v3-deal__go">{t('landing.hero.cleanCta')}</span>
</a>
)}
</div>
<ul className="v3-list">
{discounts.map((c) => (
<li key={c.code}>
<Icon name="check" size={20} strokeWidth={2.6} />
{t('plans.plus.discount', { pct: c.barevPlusDiscountPct, category: t(`categories.${c.code}.name`) })}
</li>
))}
<li>
<Icon name="check" size={20} strokeWidth={2.6} />
{t('plans.free.perks')}
</li>
</ul>
</section>
<section className="v3-final">
<p>{t('landing.v3.noCode')}</p>
<BotLink className="bc-btn bc-btn--dark">{t('landing.v3.cta')}</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>
);
}
+136
View File
@@ -0,0 +1,136 @@
import '../../variants.css';
import { createT, formatAmd, isLocale, LOCALES, type Locale } from '@barev/i18n';
import { notFound } from 'next/navigation';
import { BotLink, BotLinkProvider } from '../../../components/client';
import { VariantHeader, variantMetadata } from '../../../components/variant';
import { getCatalog } 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, '/land4', 'landing.v4.title');
}
/** Текст табло: каждая буква — отдельная перекидная пластинка, при загрузке они один раз «перещёлкиваются» по очереди */
function Flaps({ text, start = 0 }: { text: string; start?: number }) {
return (
<span className="v4-flaps" aria-label={text}>
{Array.from(text).map((ch, i) =>
ch === ' ' ? (
<span key={i} className="v4-gap" aria-hidden="true" />
) : (
<span key={i} className="v4-flap" aria-hidden="true" style={{ ['--i' as string]: start + i }}>
{ch}
</span>
),
)}
</span>
);
}
/** Версия 4 «Табло прилёта»: для тех, кто только прилетел. Услуги клуба — как рейсы на табло, CTA — посадочный талон */
export default async function Land4({ 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 = await getCatalog();
const rows = catalog.categories
.filter((c) => c.code !== 'PERKS')
.flatMap((c) => c.services)
.sort((a, b) => Number(b.active) - Number(a.active));
return (
<BotLinkProvider locale={locale} bot={catalog.botUsername}>
<div className="v4">
<div className="v-wrap">
<VariantHeader locale={locale} path="/land4" />
<section className="v4-hero">
<h1 className="v4-title">{t('landing.v4.title')}</h1>
<p className="v4-sub">{t('landing.v4.sub')}</p>
</section>
<section className="v4-board" aria-label={`${t('landing.v4.board')}: ${t('landing.v4.city')}`}>
<div className="v4-board__head">
<span className="v4-board__name">
{t('landing.v4.board')}
</span>
<span className="v4-board__city">{t('landing.v4.city')} EVN</span>
</div>
<table className="v4-table">
<thead>
<tr>
<th>{t('landing.v4.colFlight')}</th>
<th>{t('landing.v4.colService')}</th>
<th>{t('landing.v4.colPrice')}</th>
<th>{t('landing.v4.colStatus')}</th>
</tr>
</thead>
<tbody>
{rows.map((s, i) => (
<tr key={s.code} data-live={s.active}>
<td className="v4-code">BC {String(i + 1).padStart(3, '0')}</td>
<td className="v4-name">
{s.code === 'cleaning' && s.active ? (
<a href={`/${locale}/clean`}>
<Flaps text={t(`services.${s.code}.name`)} start={i * 3} />
</a>
) : (
<Flaps text={t(`services.${s.code}.name`)} start={i * 3} />
)}
</td>
<td className="v4-price">{s.active ? t('common.from', { price: amd(s.fromPrice) }) : '—'}</td>
<td>
<span className="v4-status">{s.active ? t('landing.v4.live') : t('landing.v4.soon')}</span>
</td>
</tr>
))}
</tbody>
</table>
</section>
{/* посадочный талон: корешок отрывается по пунктиру, кнопка — посадка в бота */}
<section className="v4-pass">
<div className="v4-pass__main">
<div className="v4-pass__title">{t('landing.v4.passTitle')}</div>
<dl className="v4-pass__grid">
<div>
<dt>{t('landing.v4.passenger')}</dt>
<dd>{t('landing.v4.passengerValue')}</dd>
</div>
<div>
<dt>{t('landing.v4.from')}</dt>
<dd>{t('landing.v4.fromValue')}</dd>
</div>
<div>
<dt>{t('landing.v4.to')}</dt>
<dd>{t('landing.v4.toValue')}</dd>
</div>
<div>
<dt>{t('landing.v4.gate')}</dt>
<dd>{t('landing.v4.gateValue')}</dd>
</div>
</dl>
</div>
<div className="v4-pass__stub">
<BotLink className="bc-btn v4-pass__cta">{t('landing.v4.cta')}</BotLink>
<a href={`/${locale}/clean`} className="v4-pass__link">
{t('landing.v4.clean')}
</a>
</div>
</section>
<footer className="v-footer">
<span>{t('landing.footer.brand')}</span>
<a href={`/${locale}/privacy`}>{t('landing.footer.privacy')}</a>
</footer>
</div>
</div>
</BotLinkProvider>
);
}
+67
View File
@@ -0,0 +1,67 @@
import '../../variants.css';
import { createT, isLocale, LOCALES, pickMessages, type Locale } from '@barev/i18n';
import { notFound } from 'next/navigation';
import { BotLink, BotLinkProvider } from '../../../components/client';
import { Savings } from '../../../components/savings';
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, '/land5', 'landing.v5.title');
}
/** Версия 5 «Калькулятор Barev+»: для тех, кто решает про подписку. Честная экономия за вычетом цены подписки */
export default async function Land5({ params }: { params: Promise<{ locale: string }> }) {
const { locale: raw } = await params;
if (!isLocale(raw)) notFound();
const locale: Locale = raw;
const t = createT(locale);
const [catalog, cleaning] = await Promise.all([getCatalog(), getCleaning()]);
const others = catalog.categories.filter((c) => c.code !== 'HOME' && c.barevPlusDiscountPct > 0);
const messages = pickMessages(locale, ['landing.v5.', 'params.cleaning.', 'common.']);
return (
<BotLinkProvider locale={locale} bot={catalog.botUsername}>
<div className="v5">
<div className="v-wrap">
<VariantHeader locale={locale} path="/land5" />
<section className="v5-hero">
<h1 className="v5-title">{t('landing.v5.title')}</h1>
<p className="v5-sub">{t('landing.v5.sub')}</p>
</section>
<Savings locale={locale} config={cleaning} monthlyPrice={catalog.barevPlus.monthlyPrice} messages={messages} />
<section className="v5-more">
<div>
<h2>{t('landing.v5.other')}</h2>
<ul>
{others.map((c) => (
<li key={c.code}>
<b>−{c.barevPlusDiscountPct}%</b>
{t(`categories.${c.code}.name`)}
</li>
))}
</ul>
</div>
<div className="v5-more__cta">
<BotLink className="bc-btn bc-btn--dark">{t('landing.v5.cta')}</BotLink>
<p>{t('landing.v5.gift')}</p>
</div>
</section>
<footer className="v-footer">
<span>{t('landing.footer.brand')}</span>
<a href={`/${locale}/privacy`}>{t('landing.footer.privacy')}</a>
</footer>
</div>
</div>
</BotLinkProvider>
);
}
+130
View File
@@ -0,0 +1,130 @@
import '../../variants.css';
import { quote } from '@barev/core';
import { createT, formatAmd, formatDateTime, isLocale, LOCALES, type Locale } from '@barev/i18n';
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, '/land6', 'landing.v6.tag');
}
/** Пример дохода агентства: условные клиенты и заказы, цена и процент — настоящие из каталога и настроек */
const EXAMPLE = { clients: 20, orders: 2 };
/** Печать клуба: текст по кругу и «Barev» в центре */
function Stamp() {
return (
<svg className="v6-stamp" viewBox="0 0 160 160" aria-hidden="true">
<defs>
<path id="v6-ring" d="M80 80 m-58 0 a58 58 0 1 1 116 0 a58 58 0 1 1 -116 0" />
</defs>
<circle cx="80" cy="80" r="74" fill="none" stroke="currentColor" strokeWidth="3" />
<circle cx="80" cy="80" r="44" fill="none" stroke="currentColor" strokeWidth="2" />
{/* длина текста = окружность r=58 (2π·58 ≈ 364), чтобы конец не наезжал на начало */}
<text fontSize="14" fontWeight="700" fill="currentColor">
<textPath href="#v6-ring" textLength="356" lengthAdjust="spacing">
BAREV CLUB ✦ YEREVAN ✦ PARTNER ✦
</textPath>
</text>
<text x="80" y="88" textAnchor="middle" fontSize="24" fontWeight="800" fontStyle="italic" fill="currentColor">
Barev
</text>
</svg>
);
}
/** Версия 6 «Для агентств»: B2B-страница главного канала привлечения, оформленная как договор с пунктами и печатью */
export default async function Land6({ 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()]);
const agency = catalog.agency ?? { commissionPct: 10, attributionMonths: 12 };
const price = quote(cleaning.pricing, { cleaningType: 'regular', rooms: 2, bathrooms: 1 }).totalPrice;
const amount = Math.round((EXAMPLE.clients * EXAMPLE.orders * price * agency.commissionPct) / 100);
const vars = { pct: agency.commissionPct, months: agency.attributionMonths, days: catalog.barevPlus.giftDays };
const sections = [
{ key: 's1', items: ['s1i1', 's1i2', 's1i3'] },
{ key: 's2', items: ['s2i1', 's2i2', 's2i3'] },
{ key: 's3', items: ['s3i1', 's3i2', 's3i3'] },
];
return (
<BotLinkProvider locale={locale} bot={catalog.botUsername}>
<div className="v6">
<div className="v-wrap">
<VariantHeader locale={locale} path="/land6" />
<article className="v6-doc">
<div className="v6-doc__top">
<span className="v6-tag">{t('landing.v6.tag')}</span>
<span className="v6-date">
{t('landing.v6.docDate')}, {formatDateTime(locale, new Date(), { day: 'numeric', month: 'long', year: 'numeric' })}
</span>
</div>
<h1 className="v6-title">{t('landing.v6.title')}</h1>
<p className="v6-sub">{t('landing.v6.sub')}</p>
<div className="v6-body">
<div className="v6-clauses">
{sections.map((s, i) => (
<section key={s.key} className="v6-clause">
<h2>
<span>{i + 1}.</span> {t(`landing.v6.${s.key}`)}
</h2>
<ol>
{s.items.map((item, j) => (
<li key={item}>
<span className="v6-num">
{i + 1}.{j + 1}
</span>
<span>{t(`landing.v6.${item}`, vars)}</span>
</li>
))}
</ol>
</section>
))}
</div>
<aside className="v6-example">
<div className="v6-example__title">{t('landing.v6.exampleTitle')}</div>
<div className="v6-example__calc">{t('landing.v6.exampleText', { clients: EXAMPLE.clients, orders: EXAMPLE.orders, price: amd(price) })}</div>
<div className="v6-example__pct">× {agency.commissionPct}%</div>
<div className="v6-example__result">{t('landing.v6.exampleResult', { amount: amd(amount) })}</div>
<p>{t('landing.v6.exampleNote')}</p>
</aside>
</div>
<div className="v6-sign">
<div className="v6-sign__line">
<span />
{t('landing.v6.signAgency')}
</div>
<div className="v6-sign__line v6-sign__line--us">
<Stamp />
<span />
{t('landing.v6.signUs')}
</div>
</div>
<BotLink className="bc-btn bc-btn--accent v6-cta">{t('landing.v6.cta')}</BotLink>
</article>
<footer className="v-footer">
<span>{t('landing.footer.brand')}</span>
<a href={`/${locale}/privacy`}>{t('landing.footer.privacy')}</a>
</footer>
</div>
</div>
</BotLinkProvider>
);
}
+56
View File
@@ -0,0 +1,56 @@
import '@barev/ui/styles.css';
import '../landing.css';
import { createT, isLocale, LOCALES, type Locale } from '@barev/i18n';
import type { Metadata, Viewport } from 'next';
import { Noto_Sans_Armenian, Onest, Rubik } from 'next/font/google';
import { notFound } from 'next/navigation';
import type { ReactNode } from 'react';
const rubik = Rubik({ subsets: ['latin', 'cyrillic'], weight: ['400', '500', '700', '800'], style: ['normal', 'italic'], variable: '--font-rubik', display: 'swap' });
const onest = Onest({ subsets: ['latin', 'cyrillic'], weight: ['400', '500', '600'], variable: '--font-onest', display: 'swap' });
const armenian = Noto_Sans_Armenian({ subsets: ['armenian'], weight: ['400', '600', '700'], variable: '--font-armenian', display: 'swap', preload: false });
const SITE = process.env.LANDING_URL ?? 'http://localhost:3000';
export function generateStaticParams() {
return LOCALES.map((locale) => ({ locale }));
}
export const viewport: Viewport = { themeColor: '#FBF6EE', width: 'device-width', initialScale: 1 };
/** LND-4: SEO-мета на каждом языке и hreflang */
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
const { locale } = await params;
if (!isLocale(locale)) return {};
const t = createT(locale);
// по умолчанию — заголовок и описание главной (страницы задают свои)
const title = t('landing.home.metaTitle');
const description = t('landing.home.metaDescription');
return {
metadataBase: new URL(SITE),
title,
description,
alternates: {
canonical: `/${locale}`,
languages: Object.fromEntries([...LOCALES.map((l) => [l, `/${l}`]), ['x-default', '/ru']]),
},
openGraph: { title, description, locale, type: 'website', siteName: 'Barev Club' },
twitter: { card: 'summary_large_image' },
// подтверждение прав в Search Console, Яндекс Вебмастере и Bing — коды из .env (можно и через DNS)
verification: {
google: process.env.GOOGLE_SITE_VERIFICATION || undefined,
yandex: process.env.YANDEX_VERIFICATION || undefined,
other: process.env.BING_SITE_VERIFICATION ? { 'msvalidate.01': process.env.BING_SITE_VERIFICATION } : undefined,
},
};
}
export default async function LocaleLayout({ children, params }: { children: ReactNode; params: Promise<{ locale: string }> }) {
const { locale } = await params;
if (!isLocale(locale)) notFound();
return (
<html lang={locale as Locale} className={`${rubik.variable} ${onest.variable} ${armenian.variable}`}>
<body>{children}</body>
</html>
);
}
@@ -0,0 +1,14 @@
import { createT, isLocale, LOCALES } from '@barev/i18n';
import { OG_SIZE, ogImage } from '../../../lib/og';
export const size = OG_SIZE;
export const contentType = 'image/png';
export function generateStaticParams() {
return LOCALES.map((locale) => ({ locale }));
}
/** OG-картинка «Уже живу в Армении» */
export default async function OgImage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
return ogImage(createT(isLocale(locale) ? locale : 'ru')('landing.live.title'));
}
+122
View File
@@ -0,0 +1,122 @@
import '../../variants.css';
import '../../home.css';
import { createT, formatAmd, isLocale, LOCALES, type Locale } from '@barev/i18n';
import type { Viewport } from 'next';
import { notFound } from 'next/navigation';
import { BotLink, BotLinkProvider } from '../../../components/client';
import { AudienceSwitch, ClubPlans, HomeFooter, homeMetadata } from '../../../components/home';
import { ClubJsonLd } from '../../../components/seo';
import { VariantHeader } from '../../../components/variant';
import { getCatalog } 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 homeMetadata(params, '/live');
}
export const viewport: Viewport = { themeColor: '#4A1028' };
/**
* Прайс по подписке: клининг можно заказать сразу, трансфер — со скидкой Elite, документы — «от N ֏»,
* остальное пока «скоро».
*/
const ITEMS = [
{ key: 'cleaning', value: 'order' },
{ key: 'repair', value: 'soon' },
{ key: 'transfer', value: 'pct' },
{ key: 'tours', value: 'soon' },
{ key: 'docs', value: 'from' },
{ key: 'lang', value: 'soon' },
{ key: 'career', value: 'soon' },
{ key: 'partners', value: 'soon' },
] as const;
/** Версия главной «Уже живу в Армении»: что даёт подписка — скидки по направлениям и тарифы */
export default async function Live({ params }: { params: Promise<{ locale: string }> }) {
const { locale: raw } = await params;
if (!isLocale(raw)) notFound();
const locale: Locale = raw;
const t = createT(locale);
const catalog = await getCatalog();
return (
<BotLinkProvider locale={locale} bot={catalog.botUsername}>
<div className="v2">
<ClubJsonLd locale={locale} path="/live" bot={catalog.botUsername} plusPrice={catalog.barevPlus.monthlyPrice} offer={catalog.offer} />
<div className="v-wrap">
<VariantHeader locale={locale} path="/live" className="vh--dark" logoColor="#F6C3D3" />
<AudienceSwitch locale={locale} current="live" />
<section className="v2-hero">
<div className="v2-word" aria-hidden="true">
Barev
</div>
<ul className="v2-hello" aria-hidden="true">
{t('landing.v2.hello')
.split('\n')
.map((h) => (
<li key={h}>{h}</li>
))}
</ul>
<div className="v2-hero__text">
<h1 className="v2-title">{t('landing.live.title')}</h1>
<div className="h-hero__side">
<p className="v2-sub">{t('landing.live.sub')}</p>
<div className="h-actions">
<BotLink className="bc-btn h-cta">{t('landing.live.cta')}</BotLink>
<a href="#plans" className="bc-btn h-cta-ghost">
{t('landing.live.ctaPlans')}
</a>
</div>
</div>
</div>
</section>
<section id="discounts" className="h-price" aria-labelledby="discounts-title">
<div className="h-section-head">
<h2 id="discounts-title">{t('landing.live.listTitle')}</h2>
<p>{t('landing.live.listNote', { pct: catalog.offer.plusMaxPct })}</p>
</div>
<ul className="h-price__list">
{ITEMS.map((it) => (
<li key={it.key} className="h-price__row" data-kind={it.value}>
<b className="h-price__name">{t(`landing.live.${it.key}`)}</b>
<span className="h-price__dots" aria-hidden="true" />
{it.value === 'order' ? (
<a href={`/${locale}/clean`} className="bc-btn h-price__order">
{t('landing.live.orderNow')}
</a>
) : (
<span className="h-price__value">
{it.value === 'pct'
? t('landing.live.upTo', { pct: catalog.offer.eliteMaxPct })
: it.value === 'from'
? t('landing.live.docsFrom', { price: formatAmd(locale, catalog.offer.docsFrom) })
: t('common.soon')}
</span>
)}
<small className="h-price__hint">{t(`landing.live.${it.key}Hint`)}</small>
</li>
))}
</ul>
</section>
<ClubPlans locale={locale} plusPrice={catalog.barevPlus.monthlyPrice} offer={catalog.offer} />
<section className="v2-final">
<div>
<h2>{t('landing.live.finalTitle')}</h2>
<p>{t('landing.live.finalText')}</p>
</div>
<BotLink className="bc-btn v2-final__cta">{t('landing.live.cta')}</BotLink>
</section>
<HomeFooter locale={locale} />
</div>
</div>
</BotLinkProvider>
);
}
@@ -0,0 +1,14 @@
import { createT, isLocale, LOCALES } from '@barev/i18n';
import { OG_SIZE, ogImage } from '../../lib/og';
export const size = OG_SIZE;
export const contentType = 'image/png';
export function generateStaticParams() {
return LOCALES.map((locale) => ({ locale }));
}
/** LND-4: OG-картинка главной «Переезжаю» на каждом языке — тот же заголовок, что в H1 */
export default async function OgImage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
return ogImage(createT(isLocale(locale) ? locale : 'ru')('landing.home.title'));
}
+144
View File
@@ -0,0 +1,144 @@
import '../variants.css';
import '../home.css';
import { createT, formatAmd, isLocale, LOCALES, pickMessages, type Locale } from '@barev/i18n';
import type { Viewport } from 'next';
import { notFound } from 'next/navigation';
import { BotLink, BotLinkProvider } from '../../components/client';
import { AudienceSwitch, ClubPlans, HomeFooter, homeMetadata } from '../../components/home';
import { QuizButton, QuizProvider } from '../../components/quiz';
import { ClubJsonLd } from '../../components/seo';
import { VariantHeader } from '../../components/variant';
import { getCatalog } 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 homeMetadata(params, '');
}
export const viewport: Viewport = { themeColor: '#4A1028' };
/** Шаги переезда: когда, что делаем и короткий факт (цена или срок) справа */
const STEPS = ['airport', 'flat', 'docs', 'permit', 'perks', 'chat'] as const;
const WHEN: Record<(typeof STEPS)[number], string> = {
airport: 'whenArrive',
flat: 'whenFirst',
docs: 'whenSettled',
permit: 'whenLater',
perks: 'whenDaily',
chat: 'whenAnytime',
};
/** Шаги таймлайна со своей страницей услуги — заголовок шага ведёт на неё */
const LINKS: Partial<Record<(typeof STEPS)[number], string>> = { airport: '/airport', docs: '/social-card', permit: '/residence-permit' };
/**
* Главная «Переезжаю»: огромное «Barev», таймлайн от аэропорта до ВНЖ, тарифы клуба и квиз-подбор.
* Версия для тех, кто уже живёт в Армении, — /live.
*/
export default async function Home({ 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 = await getCatalog();
const messages = pickMessages(locale, ['landing.quiz.', 'errors.']);
const meta = (key: (typeof STEPS)[number]) => {
switch (key) {
case 'airport':
return null;
case 'flat':
return null;
case 'docs':
return null;
case 'permit':
return null;
case 'perks':
// скидки партнёров ещё не запущены — вместо ссылки плашка «Скоро» в стиле «Подробнее»
return (
<span className="bc-btn h-more h-more--soon" aria-disabled="true">
{t('landing.home.perksSoon')}
</span>
);
case 'chat':
return <BotLink className="bc-btn v2-btn">{t('landing.home.chatCta')}</BotLink>;
}
};
return (
<BotLinkProvider locale={locale} bot={catalog.botUsername}>
<QuizProvider locale={locale} messages={messages} consultPrice={amd(catalog.offer.consultation)}>
<div className="v2">
<ClubJsonLd locale={locale} path="" bot={catalog.botUsername} plusPrice={catalog.barevPlus.monthlyPrice} offer={catalog.offer} />
<div className="v-wrap">
<VariantHeader locale={locale} path="" className="vh--dark" logoColor="#F6C3D3" />
<AudienceSwitch locale={locale} current="move" />
<section className="v2-hero">
<div className="v2-word" aria-hidden="true">
Barev
</div>
<ul className="v2-hello" aria-hidden="true">
{t('landing.v2.hello')
.split('\n')
.map((h) => (
<li key={h}>{h}</li>
))}
</ul>
<div className="v2-hero__text">
<h1 className="v2-title">{t('landing.home.title')}</h1>
<div className="h-hero__side">
<p className="v2-sub">{t('landing.home.sub')}</p>
<div className="h-actions">
<QuizButton className="bc-btn h-cta">{t('landing.home.cta')}</QuizButton>
<BotLink className="bc-btn h-cta-ghost">{t('landing.home.ctaChat')}</BotLink>
</div>
</div>
</div>
</section>
<ol className="v2-line">
{STEPS.map((key) => {
// квартиры (/apartments) пока есть только на русском
const link = key === 'flat' ? (locale === 'ru' ? '/apartments' : undefined) : LINKS[key];
return (
<li key={key} className="v2-step" data-live={key === 'airport' || key === 'chat' || Boolean(link && key === 'flat')}>
<div className="v2-step__when">{t(`landing.home.${WHEN[key]}`)}</div>
<div className="v2-step__body">
<h2>{link ? <a href={`/${locale}${link}`}>{t(`landing.home.${key}Title`)}</a> : t(`landing.home.${key}Title`)}</h2>
<p>{t(`landing.home.${key}Text`)}</p>
</div>
<div className="v2-step__meta">
{meta(key)}
{link && (
<a href={`/${locale}${link}`} className="bc-btn h-more">
{t('landing.home.more')} →
</a>
)}
</div>
</li>
);
})}
</ol>
<ClubPlans locale={locale} plusPrice={catalog.barevPlus.monthlyPrice} offer={catalog.offer} />
<section className="v2-final">
<div>
<h2>{t('landing.home.finalTitle')}</h2>
<p>{t('landing.home.finalText')}</p>
</div>
<QuizButton className="bc-btn v2-final__cta">{t('landing.home.cta')}</QuizButton>
</section>
<HomeFooter locale={locale} />
</div>
</div>
</QuizProvider>
</BotLinkProvider>
);
}
+117
View File
@@ -0,0 +1,117 @@
import { createT, formatAmd, isLocale, LOCALES, type Locale } from '@barev/i18n';
import { Icon } from '@barev/ui';
import { notFound } from 'next/navigation';
import { LegalShell, legalMetadata } from '../../../components/legal';
import { getCatalog } 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 legalMetadata(params, 'pricing');
}
interface Row {
id: string;
price: string;
vars?: Record<string, string>;
href?: string;
}
/**
* Цены: подписки клуба и услуги, которые сейчас продаются. Все суммы — из настроек админки
* (как на главной и страницах услуг), поэтому страница не расходится с ботом.
*/
export default async function Pricing({ 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 { offer, barevPlus, categories } = await getCatalog();
const cleaning = categories.find((c) => c.code === 'HOME')?.services.find((s) => s.code === 'cleaning')?.fromPrice ?? 0;
const from = (n: number) => t('pricing.from', { price: amd(n) });
const plans = [
{ id: 'free', name: 'Barev', price: t('landing.club.free'), pct: 0 },
{ id: 'plus', name: 'Barev+', price: t('landing.club.perMonth', { price: amd(barevPlus.monthlyPrice) }), pct: offer.plusMaxPct },
{ id: 'elite', name: 'Barev Elite', price: t('landing.club.perMonth', { price: amd(offer.elitePrice) }), pct: offer.eliteMaxPct },
] as const;
const groups: { id: 'HOME' | 'DOCS'; rows: Row[] }[] = [
{
id: 'HOME',
rows: [
{ id: 'cleaning', price: from(cleaning), href: '/clean' },
{ id: 'transferStandard', price: amd(offer.transferStandard), href: '/airport' },
{ id: 'transferPremium', price: amd(offer.transferPremium), href: '/airport' },
{ id: 'hotel', price: t('pricing.perDay', { price: from(offer.hotelWithPlus) }), vars: { base: amd(offer.hotelBase) } },
],
},
{
id: 'DOCS',
rows: [
{ id: 'socialCard', price: amd(offer.socialCard), href: '/social-card' },
{ id: 'consultation', price: amd(offer.consultation), href: '/residence-permit' },
{ id: 'residence', price: from(offer.residenceEscortFrom), vars: { fee: amd(offer.residenceStateFee) }, href: '/residence-permit' },
],
},
];
return (
<LegalShell locale={locale} page="pricing">
<section className="policy__section" id="plans">
<h2>{t('pricing.plansTitle')}</h2>
<p>{t('pricing.plansNote')}</p>
<div className="pr-plans">
{plans.map((p) => (
<article key={p.id} className="pr-plan" data-plan={p.id}>
<h3>{p.name}</h3>
<span className="pr-plan__price">{p.price}</span>
<ul>
{[1, 2, 3].map((n) => (
<li key={n}>
<Icon name="check" size={16} strokeWidth={2.6} />
{t(`landing.club.${p.id}F${n}`, { pct: p.pct })}
</li>
))}
</ul>
<small>{t(`pricing.planNote.${p.id}`, { days: barevPlus.giftDays })}</small>
</article>
))}
</div>
</section>
<section className="policy__section" id="services">
<h2>{t('pricing.servicesTitle')}</h2>
<p>{t('pricing.servicesNote')}</p>
{groups.map((g) => (
<div key={g.id} className="pr-group">
<h3>{t(`categories.${g.id}.name`)}</h3>
<dl className="pr-list">
{g.rows.map((r) => (
<div key={r.id} className="pr-row">
<dt>
{r.href ? <a href={`/${locale}${r.href}`}>{t(`pricing.rows.${r.id}.name`)}</a> : t(`pricing.rows.${r.id}.name`)}
<small>{t(`pricing.rows.${r.id}.note`, r.vars)}</small>
</dt>
<dd>{r.price}</dd>
</div>
))}
</dl>
</div>
))}
</section>
<section className="policy__section" id="payment">
<h2>{t('pricing.payTitle')}</h2>
<p>{t('pricing.payBody')}</p>
<p>
<a href={`/${locale}/terms`}>{t('landing.footer.terms')}</a> · <a href={`/${locale}/refund`}>{t('landing.footer.refund')}</a>
</p>
</section>
</LegalShell>
);
}
@@ -0,0 +1,22 @@
import { isLocale, LOCALES } from '@barev/i18n';
import { notFound } from 'next/navigation';
import { LegalDocument, legalMetadata } from '../../../components/legal';
export const dynamicParams = false;
export function generateStaticParams() {
return LOCALES.map((locale) => ({ locale }));
}
/** Разделы по порядку; тексты — privacy.<id>.title / privacy.<id>.body */
const SECTIONS = ['operator', 'data', 'purposes', 'basis', 'recipients', 'location', 'retention', 'security', 'rights', 'marketing', 'cookies', 'age', 'changes'] as const;
export function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
return legalMetadata(params, 'privacy');
}
/** ONB-3: политика конфиденциальности. Оператор — ИП владельца. Основной текст на русском, переводы needs_review */
export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
if (!isLocale(locale)) notFound();
return <LegalDocument locale={locale} ns="privacy" sections={SECTIONS} />;
}
+22
View File
@@ -0,0 +1,22 @@
import { isLocale, LOCALES } from '@barev/i18n';
import { notFound } from 'next/navigation';
import { LegalDocument, legalMetadata } from '../../../components/legal';
export const dynamicParams = false;
export function generateStaticParams() {
return LOCALES.map((locale) => ({ locale }));
}
/** Разделы по порядку; тексты — refund.<id>.title / refund.<id>.body */
const SECTIONS = ['cancelClient', 'cancelUs', 'quality', 'subscription', 'nonRefundable', 'howTo', 'timing'] as const;
export function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
return legalMetadata(params, 'refund');
}
/** Правила отмены и возврата денег. Основной текст на русском, переводы needs_review */
export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
if (!isLocale(locale)) notFound();
return <LegalDocument locale={locale} ns="refund" sections={SECTIONS} />;
}
@@ -0,0 +1,14 @@
import { createT, isLocale, LOCALES } from '@barev/i18n';
import { OG_SIZE, ogImage } from '../../../lib/og';
export const size = OG_SIZE;
export const contentType = 'image/png';
export function generateStaticParams() {
return LOCALES.map((locale) => ({ locale }));
}
/** OG-картинка страницы /residence-permit — заголовок H1 */
export default async function OgImage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
return ogImage(createT(isLocale(locale) ? locale : 'ru')('landing.svc.residence.h1'));
}
@@ -0,0 +1,24 @@
import '../../variants.css';
import '../../home.css';
import '../../service.css';
import { isLocale, LOCALES } from '@barev/i18n';
import type { Viewport } from 'next';
import { notFound } from 'next/navigation';
import { ServicePage, serviceMetadata } from '../../../components/service-page';
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 serviceMetadata(params, 'residence');
}
export const viewport: Viewport = { themeColor: '#4A1028' };
/** /residence-permit — страница услуги под поисковый запрос; тексты landing.svc.residence, цены из админки */
export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
if (!isLocale(locale)) notFound();
return <ServicePage locale={locale} id="residence" />;
}
@@ -0,0 +1,14 @@
import { createT, isLocale, LOCALES } from '@barev/i18n';
import { OG_SIZE, ogImage } from '../../../lib/og';
export const size = OG_SIZE;
export const contentType = 'image/png';
export function generateStaticParams() {
return LOCALES.map((locale) => ({ locale }));
}
/** OG-картинка страницы /social-card — заголовок H1 */
export default async function OgImage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
return ogImage(createT(isLocale(locale) ? locale : 'ru')('landing.svc.socialCard.h1'));
}
@@ -0,0 +1,24 @@
import '../../variants.css';
import '../../home.css';
import '../../service.css';
import { isLocale, LOCALES } from '@barev/i18n';
import type { Viewport } from 'next';
import { notFound } from 'next/navigation';
import { ServicePage, serviceMetadata } from '../../../components/service-page';
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 serviceMetadata(params, 'socialCard');
}
export const viewport: Viewport = { themeColor: '#4A1028' };
/** /social-card — страница услуги под поисковый запрос; тексты landing.svc.socialCard, цены из админки */
export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
if (!isLocale(locale)) notFound();
return <ServicePage locale={locale} id="socialCard" />;
}
+22
View File
@@ -0,0 +1,22 @@
import { isLocale, LOCALES } from '@barev/i18n';
import { notFound } from 'next/navigation';
import { LegalDocument, legalMetadata } from '../../../components/legal';
export const dynamicParams = false;
export function generateStaticParams() {
return LOCALES.map((locale) => ({ locale }));
}
/** Разделы по порядку; тексты — terms.<id>.title / terms.<id>.body */
const SECTIONS = ['about', 'acceptance', 'orders', 'prices', 'payment', 'cancel', 'subscription', 'perks', 'docs', 'client', 'quality', 'liability', 'content', 'changes', 'law'] as const;
export function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
return legalMetadata(params, 'terms');
}
/** Условия использования сервиса (публичная оферта). Основной текст на русском, переводы needs_review */
export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
if (!isLocale(locale)) notFound();
return <LegalDocument locale={locale} ns="terms" sections={SECTIONS} />;
}
@@ -0,0 +1,21 @@
import { crmFetch } from '../../../../../../lib/flats';
/** Фото квартиры из CRM (обрезанное там же); кешируется браузером на сутки */
export async function GET(_req: Request, { params }: { params: Promise<{ id: string; index: string }> }) {
const { id, index } = await params;
if (!/^\d{1,9}$/.test(id) || !/^\d{1,2}$/.test(index)) return new Response(null, { status: 404 });
try {
const res = await crmFetch(`/api/barev/objects/${id}/photos/${index}`, {
next: { revalidate: 86_400 },
});
if (!res.ok) return new Response(null, { status: 404 });
return new Response(res.body, {
headers: {
'content-type': 'image/jpeg',
'cache-control': 'public, max-age=86400, stale-while-revalidate=604800',
},
});
} catch {
return new Response(null, { status: 502 });
}
}
@@ -0,0 +1,50 @@
import { crmFetch } from '../../../../lib/flats';
// Заявка на просмотр со страницы /ru/apartments → CRM (клиент «BarevClub» и сообщение в группу AREV).
/** Не больше 5 заявок с одного адреса за 10 минут — от случайных повторов и ботов */
const hits = new Map<string, number[]>();
function limited(ip: string) {
const now = Date.now();
const recent = (hits.get(ip) ?? []).filter((t) => now - t < 10 * 60_000);
recent.push(now);
hits.set(ip, recent);
if (hits.size > 5000) hits.clear();
return recent.length > 5;
}
const json = (body: object, status = 200) => Response.json(body, { status });
export async function POST(req: Request) {
const ip = (req.headers.get('x-forwarded-for') ?? '').split(',')[0]!.trim() || 'unknown';
if (limited(ip)) return json({ code: 'TOO_MANY' }, 429);
let body: Record<string, unknown>;
try {
body = (await req.json()) as Record<string, unknown>;
} catch {
return json({ code: 'VALIDATION' }, 400);
}
// скрытое поле: его заполняют только боты
if (typeof body.website === 'string' && body.website) return json({ ok: true });
const str = (v: unknown) => (typeof v === 'string' && v.trim() ? v.trim() : undefined);
const payload = {
leadId: Number(body.leadId),
day: str(body.day),
time: str(body.time),
name: str(body.name),
phone: str(body.phone),
telegram: str(body.telegram),
};
try {
const res = await crmFetch('/api/barev/requests', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(payload),
});
const data = (await res.json().catch(() => ({}))) as { code?: string };
if (!res.ok) return json({ code: data.code ?? 'FAILED' }, res.status === 401 ? 503 : res.status);
return json({ ok: true });
} catch {
return json({ code: 'FAILED' }, 502);
}
}
+18
View File
@@ -0,0 +1,18 @@
import { ImageResponse } from 'next/og';
export const size = { width: 180, height: 180 };
export const contentType = 'image/png';
/** Иконка для iOS и для логотипа в выдаче: тот же знак, что в шапке сайта */
export default function AppleIcon() {
return new ImageResponse(
(
<div style={{ width: '100%', height: '100%', background: '#4A1028', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<svg width="124" height="124" viewBox="0 0 24 24" fill="none" stroke="#F6C3D3" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 21.5a9.5 9.5 0 1 0 0-19 9.5 9.5 0 0 0 0 19zM8 12.5l2.6 2.6L16 9.5" />
</svg>
</div>
),
size,
);
}
+696
View File
@@ -0,0 +1,696 @@
/* barevclub.com/clean — заказ уборки. Токены бренда из @barev/ui; главный акцент — крупная живая цена в зелёном блоке «Дом»,
второй — «чек» цвета plum с зубчатым краем. Остальное — спокойные белые карточки. */
.cl-wrap {
padding-bottom: 110px; /* место под нижнюю панель на телефоне */
}
/* ───── hero: калькулятор ───── */
.cl-hero {
display: grid;
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
gap: 28px;
padding: 40px;
border-radius: 36px;
background: var(--bc-home);
color: var(--bc-home-ink);
}
.cl-hero__main {
display: flex;
flex-direction: column;
gap: 20px;
min-width: 0;
}
.cl-title {
margin: 0;
font-family: var(--bc-font-head);
font-weight: 800;
font-size: clamp(36px, 5vw, 64px);
line-height: 0.98;
letter-spacing: -0.03em;
text-wrap: balance;
}
.cl-sub {
margin: 0;
font-size: 18px;
line-height: 1.5;
max-width: 52ch;
}
.cl-types {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.cl-type {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
padding: 14px 16px;
border-radius: 22px;
border: 2px solid transparent;
background: rgba(255, 255, 255, 0.45);
color: inherit;
text-align: left;
cursor: pointer;
font: inherit;
transition: background 0.15s ease;
}
.cl-type:hover {
background: rgba(255, 255, 255, 0.7);
}
.cl-type[aria-checked='true'] {
background: #fff;
border-color: var(--bc-home-ink);
}
.cl-type__name {
font-family: var(--bc-font-head);
font-weight: 700;
font-size: 17px;
}
.cl-type__hint {
font-size: 13px;
line-height: 1.35;
opacity: 0.8;
}
.cl-type__from {
margin-top: auto;
padding-top: 6px;
font-size: 14px;
font-weight: 600;
}
.cl-counts {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.cl-count {
display: flex;
align-items: center;
gap: 12px;
padding: 4px 4px 4px 18px;
border-radius: 999px;
background: #fff;
font-weight: 600;
}
.cl-count .bc-stepper {
padding: 0;
}
.cl-hero__price {
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-start;
gap: 12px;
min-width: 0;
}
/* главный элемент страницы: цена меняется при каждом выборе */
.cl-price {
position: relative;
overflow: hidden;
font-family: var(--bc-font-head);
font-weight: 800;
font-size: clamp(52px, 6.6vw, 100px);
line-height: 1;
letter-spacing: -0.04em;
font-variant-numeric: tabular-nums;
white-space: nowrap;
padding-bottom: 0.06em;
}
.cl-price__out {
position: absolute;
inset: 0 auto auto 0;
animation: cl-out 0.24s ease-in forwards;
}
.cl-price__in {
display: inline-block;
animation: cl-in 0.24s ease-out;
}
@keyframes cl-out {
to {
transform: translateY(-60%);
opacity: 0;
}
}
@keyframes cl-in {
from {
transform: translateY(60%);
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.cl-price__out {
display: none;
}
.cl-price__in {
animation: none;
}
}
.cl-duration {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 500;
}
.cl-plus {
padding: 8px 14px;
border-radius: 999px;
background: var(--bc-pink);
color: var(--bc-plum);
font-size: 14px;
font-weight: 600;
}
/* ───── форма + чек ───── */
.cl-body {
display: grid;
grid-template-columns: minmax(0, 1fr) 380px;
gap: 12px;
align-items: start;
}
.cl-steps {
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.cl-block {
display: flex;
flex-direction: column;
gap: 14px;
padding: 28px;
border-radius: 32px;
background: #fff;
scroll-margin-top: 16px;
}
.cl-block[data-error='true'] {
box-shadow: inset 0 0 0 2px var(--bc-danger);
}
.cl-block__title {
margin: 0;
font-family: var(--bc-font-head);
font-weight: 700;
font-size: 24px;
letter-spacing: -0.01em;
}
.cl-block__hint {
margin: -8px 0 0;
}
.cl-muted {
color: var(--bc-ink-2);
}
.cl-small {
font-size: 13px;
}
.cl-grid2 {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
/* доп. услуги: плитки; выбранная — зелёная, как «Дом» */
.cl-extras {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 8px;
}
.cl-extra {
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
min-height: 96px;
padding: 14px 16px;
border-radius: 22px;
border: 2px solid var(--bc-line);
background: #fff;
color: var(--bc-ink);
text-align: left;
font: inherit;
}
button.cl-extra {
cursor: pointer;
padding-right: 48px;
}
.cl-extra[aria-pressed='true'],
.cl-extra[data-on='true'] {
border-color: var(--bc-home);
background: color-mix(in srgb, var(--bc-home) 28%, #fff);
}
.cl-extra__name {
font-weight: 600;
line-height: 1.25;
}
.cl-extra__price {
font-size: 13px;
color: var(--bc-ink-2);
}
.cl-extra__tick {
position: absolute;
top: 12px;
right: 12px;
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: var(--bc-bg);
}
.cl-extra[aria-pressed='true'] .cl-extra__tick {
background: var(--bc-home-ink);
color: #fff;
}
.cl-extra .bc-stepper {
margin-top: auto;
background: var(--bc-bg);
}
.cl-extra .bc-stepper button {
width: 36px;
height: 36px;
background: #fff;
}
.cl-extra .bc-stepper output {
min-width: 28px;
font-size: 16px;
}
/* дата и время */
.cl-days {
display: flex;
gap: 6px;
overflow-x: auto;
padding-bottom: 4px;
scrollbar-width: thin;
}
.cl-day {
flex: none;
width: 60px;
padding: 10px 0;
border-radius: 20px;
border: 2px solid var(--bc-line);
background: #fff;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
cursor: pointer;
font: inherit;
color: var(--bc-ink);
}
.cl-day[aria-selected='true'] {
background: var(--bc-dark);
border-color: var(--bc-dark);
color: #fff;
}
.cl-day:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.cl-day__wd {
font-size: 12px;
text-transform: capitalize;
}
.cl-day__num {
font-family: var(--bc-font-head);
font-weight: 700;
font-size: 20px;
}
.cl-times {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(78px, 1fr));
gap: 6px;
}
.cl-time {
height: 44px;
border-radius: 999px;
border: 2px solid var(--bc-line);
background: #fff;
font: inherit;
font-weight: 600;
font-variant-numeric: tabular-nums;
cursor: pointer;
color: var(--bc-ink);
}
.cl-time[aria-pressed='true'] {
background: var(--bc-home);
border-color: var(--bc-home-ink);
color: var(--bc-home-ink);
}
.cl-consent {
display: flex;
align-items: flex-start;
gap: 10px;
font-size: 14px;
line-height: 1.45;
cursor: pointer;
}
.cl-consent input {
flex: none;
width: 22px;
height: 22px;
margin: 0;
accent-color: var(--bc-plum);
}
.cl-consent a {
color: var(--bc-accent);
}
/* видимый фокус для всего интерактивного на странице */
.cl-wrap :is(button, a, input, select, textarea):focus-visible {
outline: 3px solid var(--bc-accent);
outline-offset: 2px;
}
/* чек: plum, зубчатый нижний край */
.cl-receipt {
position: sticky;
top: 16px;
display: flex;
flex-direction: column;
gap: 12px;
padding: 28px 26px 40px;
border-radius: 32px 32px 0 0;
background: var(--bc-plum);
color: #fff;
--tooth: 14px;
-webkit-mask:
linear-gradient(#000 0 0) top / 100% calc(100% - var(--tooth)) no-repeat,
radial-gradient(circle at 50% 0, #000 calc(var(--tooth) - 0.5px), transparent var(--tooth)) bottom / calc(var(--tooth) * 2) var(--tooth) repeat-x;
mask:
linear-gradient(#000 0 0) top / 100% calc(100% - var(--tooth)) no-repeat,
radial-gradient(circle at 50% 0, #000 calc(var(--tooth) - 0.5px), transparent var(--tooth)) bottom / calc(var(--tooth) * 2) var(--tooth) repeat-x;
}
.cl-receipt__title {
font-family: var(--bc-font-head);
font-weight: 700;
font-size: 22px;
margin-bottom: 4px;
}
.cl-line {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 12px;
font-variant-numeric: tabular-nums;
}
.cl-line > span:last-child {
white-space: nowrap;
}
.cl-line small {
display: block;
margin-top: 2px;
font-size: 13px;
color: #e9c9d5;
}
.cl-line--extra {
font-size: 14px;
color: #f4dce5;
}
.cl-receipt__rule {
border-top: 2px dashed rgba(255, 255, 255, 0.28);
margin: 4px 0;
}
.cl-line--total {
font-family: var(--bc-font-head);
font-weight: 800;
font-size: 28px;
}
.cl-receipt__when {
margin-top: -4px;
font-size: 14px;
color: #e9c9d5;
}
.cl-receipt__when::first-letter {
text-transform: uppercase;
}
.cl-btn-pink {
background: var(--bc-pink);
color: var(--bc-plum);
font-weight: 700;
}
.cl-btn-pink:hover {
background: #fbd5e1;
color: var(--bc-plum);
}
.cl-receipt .cl-btn-pink {
margin-top: 6px;
}
.cl-receipt__error {
padding: 10px 14px;
border-radius: 16px;
background: #fff;
color: var(--bc-danger);
font-size: 14px;
font-weight: 500;
}
.cl-receipt__after {
margin: 0;
font-size: 13px;
line-height: 1.45;
color: #e9c9d5;
}
/* нижняя панель — только на телефоне */
.cl-bar {
display: none;
}
/* ───── заявка отправлена ───── */
.cl-done {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 18px;
padding: 48px 40px;
border-radius: 36px;
background: var(--bc-plum);
color: #fff;
}
.cl-done__mark {
width: 64px;
height: 64px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: var(--bc-home);
color: var(--bc-home-ink);
}
.cl-done__title {
margin: 0;
font-family: var(--bc-font-head);
font-weight: 800;
font-size: clamp(32px, 5vw, 56px);
line-height: 1;
letter-spacing: -0.03em;
}
.cl-done__text {
margin: 0;
max-width: 52ch;
font-size: 18px;
line-height: 1.5;
color: #f4dce5;
}
.cl-done__actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.cl-done__again {
color: #fff;
}
/* ───── что входит ───── */
.cl-included {
padding: 32px;
border-radius: 36px;
background: #fff;
display: flex;
flex-direction: column;
gap: 20px;
}
.cl-section-title {
margin: 0;
font-family: var(--bc-font-head);
font-weight: 700;
font-size: 34px;
letter-spacing: -0.02em;
}
.cl-zones {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.cl-zone {
padding: 22px;
border-radius: 26px;
background: var(--bc-bg);
}
.cl-zone h3 {
margin: 0 0 10px;
font-family: var(--bc-font-head);
font-weight: 700;
font-size: 20px;
}
.cl-zone ul {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 8px;
}
.cl-zone li {
position: relative;
padding-left: 22px;
line-height: 1.4;
}
.cl-zone li::before {
content: '';
position: absolute;
left: 2px;
top: 0.45em;
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--bc-home);
}
.cl-variants {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.cl-variants p {
margin: 0;
line-height: 1.5;
max-width: 60ch;
}
.cl-how__grid {
margin: 0;
padding: 0;
list-style: none;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
/* ───── адаптив ───── */
@media (max-width: 980px) {
.cl-hero,
.cl-body {
grid-template-columns: minmax(0, 1fr);
}
.cl-receipt {
position: static;
}
}
@media (max-width: 720px) {
.cl-hero {
padding: 24px;
border-radius: 28px;
gap: 20px;
}
.cl-types {
grid-template-columns: 1fr;
}
.cl-type {
display: grid;
grid-template-columns: 1fr auto;
column-gap: 12px;
}
.cl-type__hint {
grid-column: 1;
}
.cl-type__from {
grid-column: 2;
grid-row: 1 / span 2;
align-self: center;
padding-top: 0;
}
.cl-block {
padding: 22px 18px;
border-radius: 28px;
}
.cl-grid2,
.cl-zones,
.cl-variants,
.cl-how__grid {
grid-template-columns: 1fr;
}
/* доп. услуги на телефоне — список: название и цена слева, переключатель справа */
.cl-extras {
grid-template-columns: minmax(0, 1fr);
gap: 6px;
}
.cl-extra {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
column-gap: 10px;
min-height: 64px;
padding: 10px 10px 10px 16px;
}
.cl-extra__price {
grid-column: 1;
}
.cl-extra .bc-stepper {
grid-column: 2;
grid-row: 1 / span 2;
margin-top: 0;
}
button.cl-extra {
padding-right: 56px;
}
.cl-extra__tick {
top: 50%;
transform: translateY(-50%);
}
.cl-included,
.cl-done {
padding: 24px;
border-radius: 28px;
}
.cl-receipt .cl-btn-pink {
display: none;
}
.cl-bar {
position: fixed;
left: 12px;
right: 12px;
bottom: 12px;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 10px 10px 20px;
border-radius: 999px;
background: var(--bc-plum);
color: #fff;
box-shadow: 0 10px 30px rgba(74, 16, 40, 0.3);
}
.cl-bar__total {
font-family: var(--bc-font-head);
font-weight: 800;
font-size: 20px;
font-variant-numeric: tabular-nums;
}
.cl-bar__meta {
font-size: 12px;
color: #e9c9d5;
max-width: 40vw;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cl-bar .bc-btn {
height: 48px;
padding: 0 18px;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

+467
View File
@@ -0,0 +1,467 @@
/* /ru/apartments: квартиры из CRM — тёмная тема главной, карточки светлые, как клубная карта */
.fl-hero {
padding: 24px 8px 4px;
}
.fl-hero .s-lead {
max-width: 68ch;
}
.fl-filter {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding: 8px;
}
.fl-chip {
height: 40px;
padding: 0 18px;
border-radius: var(--bc-pill);
border: 2px solid color-mix(in srgb, var(--bc-pink) 45%, transparent);
background: transparent;
color: var(--bc-bg);
font: inherit;
font-weight: 600;
font-size: 15px;
cursor: pointer;
}
.fl-chip[aria-pressed='true'] {
background: var(--bc-pink);
border-color: var(--bc-pink);
color: var(--bc-plum);
}
.fl-count {
margin-left: auto;
font-size: 14px;
color: #e9c9d5;
}
.fl-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 20px;
margin: 0;
padding: 8px;
list-style: none;
}
@media (max-width: 860px) {
.fl-list {
grid-template-columns: minmax(0, 1fr);
}
}
.fl-card {
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: var(--bc-r-lg);
background: var(--bc-bg);
color: var(--bc-ink);
}
.fl-card__body {
display: flex;
flex-direction: column;
gap: 12px;
padding: 20px 22px 22px;
flex: 1;
}
.fl-card__head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
}
.fl-card__title {
margin: 0;
font-family: var(--bc-font-head);
font-weight: 700;
font-size: 22px;
line-height: 1.15;
color: var(--bc-plum);
}
.fl-card__facts {
margin: 6px 0 0;
font-size: 15px;
color: #6b5d58;
}
.fl-card__price {
margin: 0;
flex-shrink: 0;
font-family: var(--bc-font-head);
font-weight: 800;
font-size: 20px;
line-height: 1.15;
text-align: right;
color: var(--bc-accent);
white-space: nowrap;
}
@media (max-width: 420px) {
.fl-card__head {
flex-direction: column;
gap: 6px;
}
.fl-card__price {
text-align: left;
}
}
.fl-text p {
margin: 0;
white-space: pre-line;
font-size: 15px;
line-height: 1.55;
overflow-wrap: anywhere;
}
.fl-text[data-open='false'] p {
display: -webkit-box;
-webkit-line-clamp: 7;
-webkit-box-orient: vertical;
overflow: hidden;
}
.fl-more {
align-self: flex-start;
padding: 0;
border: 0;
background: none;
color: var(--bc-accent);
font: inherit;
font-weight: 600;
font-size: 15px;
cursor: pointer;
text-decoration: underline;
text-underline-offset: 3px;
}
.fl-book {
width: 100%;
margin-top: auto;
background: var(--bc-plum);
color: #fff;
}
.fl-card .fl-book {
margin-top: 8px;
}
/* галерея */
.fl-gallery {
position: relative;
aspect-ratio: 4 / 3;
background: #2f0a19;
}
.fl-gallery--empty {
display: grid;
place-items: center;
color: #e9c9d5;
font-size: 15px;
}
.fl-gallery__track {
display: flex;
height: 100%;
overflow-x: auto;
scroll-snap-type: x mandatory;
overscroll-behavior-x: contain;
scrollbar-width: none;
}
.fl-gallery__track::-webkit-scrollbar {
display: none;
}
.fl-gallery__slide {
flex: 0 0 100%;
height: 100%;
padding: 0;
border: 0;
background: none;
scroll-snap-align: start;
cursor: zoom-in;
}
.fl-gallery__slide img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.fl-gallery__nav {
position: absolute;
top: 50%;
translate: 0 -50%;
width: 44px;
height: 44px;
border: 0;
border-radius: 50%;
background: rgba(251, 246, 238, 0.92);
color: var(--bc-plum);
font-size: 30px;
line-height: 1;
cursor: pointer;
box-shadow: var(--bc-shadow);
}
.fl-gallery__nav[disabled] {
opacity: 0;
pointer-events: none;
}
.fl-gallery__nav--prev {
left: 12px;
}
.fl-gallery__nav--next {
right: 12px;
}
.fl-gallery__count {
position: absolute;
right: 12px;
bottom: 12px;
padding: 4px 10px;
border-radius: var(--bc-pill);
background: rgba(30, 27, 26, 0.7);
color: #fff;
font-size: 13px;
font-variant-numeric: tabular-nums;
}
@media (hover: none) {
.fl-gallery:not(.fl-gallery--full) .fl-gallery__nav {
display: none;
}
}
/* фото на весь экран */
.fl-viewer {
width: 100vw;
max-width: none;
height: 100dvh;
max-height: none;
margin: 0;
padding: 0;
border: 0;
background: #120509;
color: #fff;
}
.fl-viewer[open] {
display: flex;
flex-direction: column;
}
.fl-viewer::backdrop {
background: #120509;
}
.fl-viewer__bar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
padding: 12px 16px;
font-size: 15px;
}
.fl-gallery--full {
flex: 1;
aspect-ratio: auto;
min-height: 0;
background: none;
}
.fl-gallery--full .fl-gallery__slide {
cursor: default;
}
.fl-gallery--full .fl-gallery__slide img {
object-fit: contain;
}
.fl-viewer__book {
width: auto;
margin: 12px 16px 16px;
background: var(--bc-pink);
color: var(--bc-plum);
}
.fl-close {
flex-shrink: 0;
width: 44px;
height: 44px;
border: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.12);
color: inherit;
font-size: 28px;
line-height: 1;
cursor: pointer;
}
/* запись на просмотр */
.fl-modal {
width: min(560px, calc(100vw - 24px));
max-height: calc(100dvh - 24px);
padding: 0;
border: 0;
border-radius: var(--bc-r-lg);
background: var(--bc-bg);
color: var(--bc-ink);
}
.fl-modal::backdrop {
background: rgba(30, 10, 18, 0.7);
}
.fl-modal__box {
position: relative;
padding: 28px 24px 24px;
}
.fl-modal__close {
position: absolute;
top: 12px;
right: 12px;
background: rgba(74, 16, 40, 0.08);
color: var(--bc-plum);
}
.fl-modal h2 {
margin: 0 48px 0 0;
font-family: var(--bc-font-head);
font-weight: 800;
font-size: 26px;
color: var(--bc-plum);
}
.fl-modal__flat {
margin: 6px 0 18px;
color: #6b5d58;
font-size: 15px;
}
.fl-field {
margin: 0 0 16px;
padding: 0;
border: 0;
min-width: 0;
}
.fl-field legend,
.fl-input span {
display: block;
margin-bottom: 8px;
font-weight: 600;
font-size: 14px;
color: var(--bc-plum);
}
.fl-days {
display: flex;
gap: 8px;
overflow-x: auto;
padding-bottom: 4px;
scrollbar-width: thin;
}
.fl-times {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.fl-day input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.fl-day span {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
min-width: 72px;
min-height: 60px;
padding: 8px 10px;
border-radius: var(--bc-r-sm);
border: 2px solid var(--bc-line);
background: var(--bc-surface);
cursor: pointer;
text-align: center;
}
.fl-day small {
font-size: 12px;
color: #6b5d58;
}
.fl-day b {
font-size: 15px;
white-space: nowrap;
}
.fl-day input:checked + span {
border-color: var(--bc-plum);
background: var(--bc-plum);
color: #fff;
}
.fl-day input:checked + span small {
color: #f4dce5;
}
.fl-day input:focus-visible + span {
outline: 3px solid var(--bc-accent);
outline-offset: 2px;
}
.fl-input {
display: block;
margin-bottom: 12px;
}
.fl-input input {
width: 100%;
height: 50px;
padding: 0 16px;
border-radius: 14px;
border: 2px solid var(--bc-line);
background: var(--bc-surface);
font: inherit;
font-size: 16px;
color: var(--bc-ink);
}
.fl-input input:focus {
outline: none;
border-color: var(--bc-plum);
}
.fl-contacts {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
@media (max-width: 480px) {
.fl-contacts {
grid-template-columns: 1fr;
gap: 0;
}
}
.fl-hp {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
}
.fl-error {
margin: 4px 0 12px;
padding: 10px 14px;
border-radius: 12px;
background: color-mix(in srgb, var(--bc-danger) 10%, transparent);
color: var(--bc-danger);
font-size: 14px;
}
.fl-error a {
color: inherit;
font-weight: 600;
}
.fl-note {
margin: 12px 0 0;
font-size: 12px;
line-height: 1.45;
color: #6b5d58;
}
.fl-note a {
color: inherit;
}
.fl-done p {
margin: 12px 0 20px;
font-size: 16px;
line-height: 1.5;
}
.fl-empty {
margin: 8px;
padding: 32px 24px;
border-radius: var(--bc-r-lg);
background: color-mix(in srgb, var(--bc-pink) 12%, transparent);
}
.fl-empty h2 {
margin: 0;
font-family: var(--bc-font-head);
font-size: 24px;
}
.fl-empty p {
margin: 10px 0 20px;
color: #f4dce5;
}
.fl-empty .fl-book {
width: auto;
background: var(--bc-pink);
color: var(--bc-plum);
}
+363
View File
@@ -0,0 +1,363 @@
/* /ru/flower — цветы по подписке. Продолжение главной v2: plum, огромное слово, таймлайн и тарифы-карты.
Здесь только своё: наклейка с ценой, переключатель частоты, карты S/M/L, сезоны, подарок и гарантия. */
/* ───── hero: «Цветы» + наклейка «от 8 900 ֏» ───── */
.fw-word {
position: relative;
justify-self: start;
font-size: clamp(84px, 15vw, 220px);
}
.fw-word__sticker {
position: absolute;
top: -8px;
right: -64px;
font-style: normal;
letter-spacing: 0;
}
.fw-word__sticker .bc-flower > span {
line-height: 1.05;
text-align: center;
max-width: 76%;
white-space: pre-line;
}
.fw-next {
margin: 0;
font-size: 15px;
color: #e9c9d5;
}
.fw-next svg {
vertical-align: -3px;
margin-right: 6px;
}
.fw-next b {
color: var(--bc-bg);
font-weight: 600;
}
/* ───── переключатель частоты: как «Переезжаю / Уже живу» ───── */
.fw-freq {
display: inline-flex;
flex-wrap: wrap;
align-self: flex-start;
gap: 4px;
padding: 4px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.06);
}
.fw-freq button {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 44px;
padding: 0 18px;
border: 0;
border-radius: 999px;
background: transparent;
font-family: var(--bc-font-head);
font-weight: 600;
font-size: 16px;
color: var(--bc-bg);
cursor: pointer;
}
.fw-freq button:hover {
color: var(--bc-pink);
}
.fw-freq button[aria-checked='true'] {
background: var(--bc-pink);
color: var(--bc-plum);
}
.fw-freq__save {
padding: 2px 8px;
border-radius: 999px;
background: var(--bc-home);
color: var(--bc-home-ink);
font-size: 12px;
font-weight: 700;
}
/* ───── карты S / M / L ───── */
.fw-plan {
position: relative;
}
.fw-plan__badge {
position: absolute;
z-index: 1;
top: -12px;
right: 16px;
padding: 5px 12px;
border-radius: 999px;
background: var(--bc-home);
color: var(--bc-home-ink);
font-size: 13px;
font-weight: 700;
transform: rotate(3deg);
}
.fw-plan__photo {
display: block;
width: 100%;
height: auto;
aspect-ratio: 1;
object-fit: cover;
border-radius: 24px;
}
.fw-card {
grid-template-areas: 'club chip' '. .' 'name name' 'price price';
}
/* вместо «чипа» банковской карты — крупная буква размера */
.fw-card__letter {
grid-area: chip;
font-family: var(--bc-font-head);
font-weight: 800;
font-style: italic;
font-size: 56px;
line-height: 0.8;
letter-spacing: -0.04em;
}
.fw-card .h-card__price small {
font-weight: 500;
font-size: 14px;
opacity: 0.75;
}
.fw-plan[data-size='s'] .h-card {
border: 1.5px solid rgba(246, 195, 211, 0.45);
color: var(--bc-bg);
}
.fw-plan[data-size='m'] .h-card {
background: var(--bc-pink);
}
.fw-plan[data-size='l'] .h-card {
background: var(--bc-perks);
}
.fw-plan[data-size='m'] .h-plan__cta {
background: var(--bc-pink);
border-color: var(--bc-pink);
color: var(--bc-plum);
}
.fw-plan[data-size='l'] .h-plan__cta {
background: var(--bc-perks);
border-color: var(--bc-perks);
color: var(--bc-plum);
}
.fw-plan[data-size='l'] .h-plan__list svg {
color: var(--bc-perks);
}
.fw-plan[data-size='m'] .h-plan__cta:hover,
.fw-plan[data-size='l'] .h-plan__cta:hover {
opacity: 0.92;
}
.fw-plan__facts {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: -6px;
}
.fw-plan__facts span {
padding: 5px 12px;
border-radius: 999px;
border: 1.5px solid rgba(246, 195, 211, 0.3);
font-size: 13px;
font-weight: 600;
color: #f4dce5;
}
.fw-plan__anchor {
margin: -6px 4px 0;
font-size: 14px;
color: #d9b4c3;
}
.fw-plan__anchor s {
text-decoration-thickness: 1.5px;
}
.fw-plan__month {
margin: 0 4px;
font-family: var(--bc-font-head);
font-weight: 600;
font-size: 16px;
}
.fw-plan__month b {
color: var(--bc-home);
font-weight: 600;
}
.fw-plans__note {
margin: 0;
padding: 0 8px;
font-size: 14px;
color: #d9b4c3;
}
/* ───── сезоны ───── */
.fw-section {
display: flex;
flex-direction: column;
gap: 28px;
margin-top: 56px;
}
.fw-seasons {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.fw-season {
display: flex;
flex-direction: column;
gap: 8px;
min-height: 200px;
padding: 26px;
border-radius: 32px;
}
.fw-season h3 {
margin: 0;
font-family: var(--bc-font-head);
font-weight: 800;
font-style: italic;
font-size: 30px;
letter-spacing: -0.02em;
}
.fw-season p {
margin: auto 0 0;
font-size: 15px;
line-height: 1.5;
}
/* ───── подарок из-за рубежа + гарантия ───── */
.fw-duo {
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
gap: 12px;
margin-top: 56px;
}
.fw-gift,
.fw-guarantee {
display: flex;
flex-direction: column;
gap: 14px;
padding: 36px;
border-radius: 36px;
}
.fw-gift {
background: var(--bc-blue);
color: var(--bc-blue-ink);
}
.fw-guarantee {
background: var(--bc-home);
color: var(--bc-home-ink);
}
.fw-gift h2,
.fw-guarantee h2 {
margin: 0;
font-family: var(--bc-font-head);
font-weight: 800;
font-size: clamp(28px, 3vw, 40px);
line-height: 1.02;
letter-spacing: -0.03em;
text-wrap: balance;
}
.fw-gift p,
.fw-guarantee p {
margin: 0;
max-width: 52ch;
font-size: 16px;
line-height: 1.5;
}
.fw-kicker {
font-family: var(--bc-font-head);
font-weight: 700;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.06em;
opacity: 0.75;
}
.fw-gift__prices {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.fw-gift__prices div {
display: flex;
flex-direction: column;
padding: 12px 18px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.55);
}
.fw-gift__prices b {
font-family: var(--bc-font-head);
font-weight: 800;
font-style: italic;
font-size: 30px;
line-height: 1;
}
.fw-gift__prices span {
margin-top: 4px;
font-size: 13px;
}
.fw-gift__cta {
align-self: flex-start;
margin-top: 6px;
height: 52px;
background: var(--bc-blue-ink);
color: #fff;
}
.fw-gift__cta:hover {
color: #fff;
opacity: 0.92;
}
/* ───── вопросы: розовый блок с белыми карточками из landing.css ───── */
.fw-faq-wrap {
display: flex;
flex-direction: column;
gap: 28px;
margin-top: 56px;
}
.fw-faq {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
align-items: start;
gap: 10px;
}
@media (max-width: 1000px) {
.fw-seasons {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 900px) {
.fw-duo {
grid-template-columns: minmax(0, 1fr);
}
}
@media (max-width: 720px) {
.fw-word__sticker {
top: auto;
bottom: -18px;
right: -40px;
}
.fw-word__sticker .bc-flower {
width: 88px !important;
height: 88px !important;
}
.fw-word__sticker .bc-flower svg {
width: 88px;
height: 88px;
}
.fw-freq {
align-self: stretch;
}
.fw-freq button {
flex: 1 1 0;
justify-content: center;
padding: 0 10px;
font-size: 14px;
}
.fw-seasons,
.fw-faq {
grid-template-columns: minmax(0, 1fr);
}
.fw-season {
min-height: 0;
}
.fw-gift,
.fw-guarantee {
padding: 26px 22px;
border-radius: 28px;
}
}
+746
View File
@@ -0,0 +1,746 @@
/* Главная (/ и /live): продолжение версии v2 — plum, огромное «Barev», таймлайн.
Здесь только то, чего нет в variants.css: переключатель аудитории, тарифы-карты, прайс скидок и квиз. */
.h-switch {
display: inline-flex;
align-self: flex-start;
gap: 4px;
padding: 4px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.06);
}
.h-switch a {
padding: 10px 18px;
border-radius: 999px;
font-family: var(--bc-font-head);
font-weight: 600;
font-size: 16px;
color: var(--bc-bg);
text-decoration: none;
}
.h-switch a:hover {
color: var(--bc-pink);
}
.h-switch a[aria-current='page'] {
background: var(--bc-pink);
color: var(--bc-plum);
}
.h-hero__side {
display: flex;
flex-direction: column;
gap: 22px;
}
.h-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.h-cta,
.h-cta-ghost {
height: 56px;
padding: 0 28px;
font-size: 17px;
}
.h-cta {
background: var(--bc-pink);
color: var(--bc-plum);
}
.h-cta:hover {
color: var(--bc-plum);
opacity: 0.92;
}
.h-cta-ghost {
background: transparent;
border: 1.5px solid rgba(246, 195, 211, 0.5);
color: var(--bc-bg);
}
.h-cta-ghost:hover {
border-color: var(--bc-pink);
color: var(--bc-pink);
}
/* шаг «Прилёт»: цена с подпиской и без — столбиком */
.v2-step__meta:has(.h-chip-note) {
flex-direction: column;
align-items: flex-end;
gap: 6px;
}
.h-chip-note {
font-size: 13px;
color: #e9c9d5;
}
.h-section-head {
display: flex;
flex-direction: column;
gap: 8px;
padding: 0 8px;
}
.h-section-head h2 {
margin: 0;
font-family: var(--bc-font-head);
font-weight: 800;
font-size: clamp(32px, 4vw, 52px);
line-height: 1;
letter-spacing: -0.03em;
}
.h-section-head p {
margin: 0;
max-width: 52ch;
font-size: 17px;
line-height: 1.5;
color: #e9c9d5;
}
/* ═════════ тарифы: клубные карты ═════════ */
.h-plans {
display: flex;
flex-direction: column;
gap: 28px;
margin-top: 40px;
}
.h-plans__grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 28px;
}
.h-plan {
display: flex;
flex-direction: column;
gap: 20px;
}
.h-card {
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: auto 1fr auto auto;
grid-template-areas: 'club chip' '. .' 'name name' 'price price';
aspect-ratio: 1.586;
padding: 20px 22px;
border-radius: 18px;
color: var(--bc-plum);
}
.h-plan[data-plan='free'] .h-card {
border: 1.5px solid rgba(246, 195, 211, 0.45);
color: var(--bc-bg);
}
.h-plan[data-plan='plus'] .h-card {
background: var(--bc-pink);
}
.h-plan[data-plan='elite'] .h-card {
background: var(--bc-perks);
}
.h-card__club {
grid-area: club;
font-size: 13px;
font-weight: 600;
opacity: 0.8;
}
/* контактная площадка карты */
.h-card__chip {
grid-area: chip;
width: 42px;
height: 32px;
border-radius: 8px;
background: color-mix(in srgb, currentColor 16%, transparent);
border: 1.5px solid color-mix(in srgb, currentColor 30%, transparent);
}
.h-card__name {
grid-area: name;
margin: 0;
font-family: var(--bc-font-head);
font-weight: 800;
font-style: italic;
font-size: clamp(30px, 3vw, 42px);
line-height: 1;
letter-spacing: -0.03em;
}
.h-card__price {
grid-area: price;
margin-top: 6px;
font-family: var(--bc-font-head);
font-weight: 600;
font-size: 18px;
}
.h-plan__list {
display: flex;
flex-direction: column;
gap: 10px;
margin: 0;
padding: 0 4px;
list-style: none;
}
.h-plan__list li {
display: flex;
gap: 10px;
align-items: flex-start;
line-height: 1.4;
color: #f4dce5;
}
.h-plan__list svg {
flex: none;
margin-top: 1px;
color: var(--bc-pink);
}
.h-plan[data-plan='elite'] .h-plan__list svg {
color: var(--bc-perks);
}
.h-plan__cta {
margin-top: auto;
height: 52px;
background: transparent;
border: 1.5px solid rgba(246, 195, 211, 0.5);
color: var(--bc-bg);
}
.h-plan__cta:hover {
color: var(--bc-pink);
border-color: var(--bc-pink);
}
.h-plan[data-plan='plus'] .h-plan__cta {
background: var(--bc-pink);
border-color: var(--bc-pink);
color: var(--bc-plum);
}
.h-plan[data-plan='elite'] .h-plan__cta {
background: var(--bc-perks);
border-color: var(--bc-perks);
color: var(--bc-plum);
}
.h-plan[data-plan='plus'] .h-plan__cta:hover,
.h-plan[data-plan='elite'] .h-plan__cta:hover {
opacity: 0.92;
}
/* ═════════ /live: прайс скидок с отточиями ═════════ */
.h-price {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 16px;
}
.h-price__list {
margin: 0;
padding: 0;
list-style: none;
}
.h-price__row {
display: grid;
grid-template-columns: auto minmax(24px, 1fr) auto;
grid-template-areas: 'name dots value' 'hint hint hint';
align-items: baseline;
column-gap: 14px;
row-gap: 4px;
padding: 20px 8px;
border-top: 1px solid rgba(246, 195, 211, 0.16);
}
.h-price__name {
grid-area: name;
font-family: var(--bc-font-head);
font-weight: 700;
font-size: clamp(22px, 2.6vw, 32px);
letter-spacing: -0.01em;
}
.h-price__dots {
grid-area: dots;
border-bottom: 2px dotted rgba(246, 195, 211, 0.4);
}
.h-price__value {
grid-area: value;
font-family: var(--bc-font-head);
font-weight: 800;
font-style: italic;
font-size: clamp(24px, 3vw, 38px);
letter-spacing: -0.02em;
color: var(--bc-pink);
white-space: nowrap;
}
.h-price__row[data-kind='from'] .h-price__value {
font-weight: 700;
font-style: normal;
font-size: clamp(18px, 2vw, 24px);
color: var(--bc-perks);
}
.h-price__row[data-kind='soon'] .h-price__name,
.h-price__row[data-kind='soon'] .h-price__hint {
opacity: 0.6;
}
.h-price__row[data-kind='soon'] .h-price__value {
font-weight: 600;
font-style: normal;
font-size: 16px;
color: #e9c9d5;
}
.h-price__order {
grid-area: value;
align-self: center;
height: 46px;
padding: 0 22px;
background: var(--bc-home);
color: var(--bc-home-ink);
white-space: nowrap;
}
.h-price__order:hover {
color: var(--bc-home-ink);
opacity: 0.9;
}
.h-price__hint {
grid-area: hint;
font-size: 15px;
color: #e9c9d5;
}
.h-footer__brand {
display: flex;
flex-direction: column;
gap: 4px;
}
.h-footer__legal {
font-size: 12px;
font-weight: 500;
opacity: 0.8;
}
.h-footer__links {
display: flex;
flex-wrap: wrap;
gap: 8px 20px;
}
@media (max-width: 900px) {
.h-plans__grid {
grid-template-columns: minmax(0, 1fr);
max-width: 440px;
}
}
@media (max-width: 720px) {
.h-switch {
align-self: stretch;
}
.h-switch a {
flex: 1 1 0;
min-width: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 8px 10px;
font-size: 14px;
line-height: 1.2;
text-align: center;
}
.h-cta,
.h-cta-ghost {
flex: 1 1 100%;
}
.v2-step__meta:has(.h-chip-note) {
align-items: flex-start;
}
.h-section-head,
.h-price__row {
padding-left: 0;
padding-right: 0;
}
}
/* ═════════ квиз ═════════ */
body:has(.q[open]) {
overflow: hidden;
}
.q {
width: 100%;
max-width: none;
height: 100%;
max-height: none;
margin: 0;
padding: 16px;
border: 0;
background: transparent;
}
.q[open] {
display: flex;
align-items: center;
justify-content: center;
}
.q::backdrop {
background: rgba(36, 8, 20, 0.62);
backdrop-filter: blur(4px);
}
.q-panel {
width: min(560px, 100%);
max-height: 100%;
overflow: auto;
padding: 18px 28px 30px;
border-radius: 32px;
background: var(--bc-bg);
color: var(--bc-ink);
}
.q :is(a, button, input):focus-visible {
outline: 3px solid var(--bc-accent);
outline-offset: 2px;
}
.q-top {
display: grid;
grid-template-columns: 44px 1fr 44px;
align-items: center;
}
.q-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border: 0;
border-radius: 50%;
background: #fff;
color: var(--bc-plum);
cursor: pointer;
}
/* прогресс — те же точки на оси, что в таймлайне главной */
.q-dots {
display: flex;
justify-content: center;
margin: 0;
padding: 0;
list-style: none;
}
.q-dots li {
position: relative;
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid var(--bc-plum);
transition: background-color 0.2s, box-shadow 0.2s;
}
.q-dots li + li {
margin-left: 26px;
}
.q-dots li + li::before {
content: '';
position: absolute;
right: calc(100% + 2px);
top: 50%;
width: 26px;
height: 2px;
margin-top: -1px;
background: rgba(74, 16, 40, 0.2);
}
.q-dots li[data-state='past'] {
background: var(--bc-plum);
}
.q-dots li[data-state='past']::before,
.q-dots li[data-state='now']::before {
background: var(--bc-plum);
}
.q-dots li[data-state='now'] {
background: var(--bc-accent);
border-color: var(--bc-accent);
box-shadow: 0 0 0 5px rgba(184, 35, 90, 0.18);
}
.q-step {
animation: q-in 0.32s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.q-step[data-dir='back'] {
animation-name: q-in-back;
}
@keyframes q-in {
from {
opacity: 0;
transform: translateX(28px);
}
}
@keyframes q-in-back {
from {
opacity: 0;
transform: translateX(-28px);
}
}
.q-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.q-title {
margin: 26px 0 4px;
font-family: var(--bc-font-head);
font-weight: 800;
font-size: clamp(26px, 4vw, 34px);
line-height: 1.08;
letter-spacing: -0.02em;
color: var(--bc-plum);
text-wrap: balance;
}
.q-title:focus {
outline: none;
}
.q-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.q-field > span {
font-size: 14px;
font-weight: 600;
color: var(--bc-ink-2);
}
.q-field small,
.q-error {
margin: 0;
font-size: 14px;
font-weight: 600;
color: var(--bc-danger);
}
.q-input {
width: 100%;
height: 54px;
padding: 0 16px;
border: 1.5px solid var(--bc-line);
border-radius: 16px;
background: #fff;
font: inherit;
font-size: 17px;
color: var(--bc-ink);
}
.q-input:focus {
outline: none;
border-color: var(--bc-plum);
}
.q-field[data-error='true'] .q-input {
border-color: var(--bc-danger);
}
.q-primary {
width: 100%;
height: 56px;
margin-top: 4px;
background: var(--bc-plum);
color: #fff;
font-size: 17px;
}
.q-primary:hover {
color: #fff;
opacity: 0.92;
}
.q-primary:disabled {
opacity: 0.6;
}
.q-consent {
margin: 0;
font-size: 13px;
line-height: 1.45;
color: var(--bc-ink-3);
}
.q-consent a {
color: var(--bc-plum);
}
.q-choices {
display: flex;
flex-direction: column;
gap: 10px;
}
.q-choice {
display: flex;
flex-direction: column;
gap: 4px;
width: 100%;
padding: 18px 20px;
border: 1.5px solid var(--bc-line);
border-radius: 20px;
background: #fff;
font: inherit;
font-size: 17px;
color: var(--bc-ink);
text-align: left;
cursor: pointer;
transition: border-color 0.15s, background-color 0.15s;
}
.q-choice:hover {
border-color: var(--bc-plum);
}
.q-choice[aria-checked='true'] {
border-color: var(--bc-plum);
background: var(--bc-pink);
}
.q-choice:disabled {
opacity: 0.6;
cursor: progress;
}
.q-choice--tall > span:last-child {
font-size: 15px;
line-height: 1.4;
color: var(--bc-ink-2);
}
.q-choice__head {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 12px;
}
.q-price {
font-family: var(--bc-font-head);
font-weight: 700;
color: var(--bc-plum);
white-space: nowrap;
}
.q-row {
display: flex;
flex-direction: column;
gap: 6px;
padding-top: 14px;
border-top: 1px solid var(--bc-line);
}
.q-row__head {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 10px;
}
.q-row__label {
font-size: 16px;
font-weight: 600;
}
.q-row__hint {
font-size: 13px;
color: var(--bc-ok);
}
.q-seg {
display: inline-flex;
padding: 3px;
border: 1.5px solid var(--bc-line);
border-radius: 999px;
background: #fff;
}
.q-seg button {
min-height: 38px;
padding: 0 14px;
border: 0;
border-radius: 999px;
background: transparent;
font: inherit;
font-size: 14px;
font-weight: 600;
color: var(--bc-ink-2);
cursor: pointer;
}
.q-seg button[aria-checked='true'] {
background: var(--bc-plum);
color: #fff;
}
.q-budget {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin: 6px 0 0;
padding: 0;
border: 0;
}
.q-budget legend {
margin-bottom: 6px;
padding: 0;
font-size: 13px;
color: var(--bc-ink-2);
}
.q-budget label {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 12px;
color: var(--bc-ink-3);
}
.q-budget .q-input {
height: 46px;
}
.q-done {
align-items: center;
text-align: center;
padding-top: 8px;
}
.q-done .q-title {
margin-top: 4px;
}
.q-done__mark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
margin-top: 18px;
border-radius: 50%;
background: var(--bc-home);
color: var(--bc-home-ink);
}
.q-lead {
margin: 0;
font-size: 17px;
line-height: 1.5;
color: var(--bc-ink-2);
}
.q-link {
padding: 10px;
border: 0;
background: none;
font: inherit;
font-size: 15px;
font-weight: 600;
color: var(--bc-plum);
text-decoration: underline;
cursor: pointer;
}
@media (max-width: 560px) {
.q {
padding: 0;
}
.q[open] {
align-items: flex-end;
}
.q-panel {
max-height: 94%;
padding: 14px 18px 28px;
border-radius: 28px 28px 0 0;
}
}
@media (prefers-reduced-motion: reduce) {
.q-step {
animation: none;
}
}
/* заголовок шага таймлайна — ссылка на страницу услуги, подчёркивание только при наведении */
.v2-step__body h2 a {
color: inherit;
text-decoration: none;
}
.v2-step__body h2 a:hover {
color: var(--bc-pink);
text-decoration: underline;
text-underline-offset: 6px;
}
/* кнопка «Подробнее →» в шаге таймлайна */
.h-more {
height: 40px;
padding: 0 18px;
font-size: 15px;
background: transparent;
border: 1.5px solid var(--bc-pink);
color: var(--bc-pink);
white-space: nowrap;
}
.h-more:hover {
background: var(--bc-pink);
color: var(--bc-plum);
}
.h-more--soon,
.h-more--soon:hover {
background: transparent;
color: var(--bc-pink);
opacity: 0.6;
cursor: default;
}
/* ссылки на услуги в подвале */
.h-footer__services {
display: flex;
flex-wrap: wrap;
gap: 6px 18px;
width: 100%;
padding-top: 12px;
border-top: 1px solid rgba(246, 195, 211, 0.16);
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><rect width="48" height="48" rx="12" fill="#4A1028"/><g transform="translate(6 6) scale(1.5)" fill="none" stroke="#F6C3D3" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21.5a9.5 9.5 0 1 0 0-19 9.5 9.5 0 0 0 0 19zM8 12.5l2.6 2.6L16 9.5"/></g></svg>

After

Width:  |  Height:  |  Size: 341 B

+635
View File
@@ -0,0 +1,635 @@
/* next/font даёт хешированные имена семейств — подменяем токены шрифтов */
:root {
--bc-font-head: var(--font-rubik), var(--font-armenian), 'PingFang SC', 'Noto Sans SC', 'Microsoft YaHei', system-ui, sans-serif;
--bc-font-body: var(--font-onest), var(--font-armenian), 'PingFang SC', 'Noto Sans SC', 'Microsoft YaHei', system-ui, sans-serif;
}
:root:lang(hy),
:root:lang(zh) {
--bc-font-head: var(--font-rubik), var(--font-armenian), 'PingFang SC', 'Noto Sans SC', 'Microsoft YaHei', system-ui, sans-serif;
--bc-font-body: var(--font-onest), var(--font-armenian), 'PingFang SC', 'Noto Sans SC', 'Microsoft YaHei', system-ui, sans-serif;
}
a:hover {
color: var(--bc-accent);
}
.wrap {
max-width: 1240px;
margin: 0 auto;
padding: 16px 20px 32px;
display: flex;
flex-direction: column;
gap: 12px;
}
.row {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 8px 8px 8px 20px;
border-radius: 999px;
background: #fff;
}
.nav {
display: flex;
flex-wrap: wrap;
gap: 20px;
font-size: 15px;
font-weight: 500;
}
.nav a {
text-decoration: none;
}
.langs {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
}
.lang {
min-width: 44px;
height: 44px;
padding: 0 10px;
border-radius: 999px;
border: none;
font-size: 13px;
font-weight: 600;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
background: transparent;
color: var(--bc-ink);
}
.lang[aria-current='true'] {
background: var(--bc-pink);
color: var(--bc-plum);
}
/* мобильное меню языков и короткая подпись кнопки Telegram: включаются на узком экране */
.langs__row {
display: contents;
}
.lang-menu,
.tg-short {
display: none;
}
.lang-menu {
position: relative;
}
.lang-menu__current {
gap: 6px;
list-style: none;
background: rgba(30, 27, 26, 0.06);
}
.lang-menu__current::-webkit-details-marker {
display: none;
}
.lang-menu[open] .lang-menu__current svg {
transform: rotate(180deg);
}
.lang-menu__list {
position: absolute;
top: calc(100% + 8px);
right: 0;
z-index: 20;
display: flex;
flex-direction: column;
gap: 2px;
padding: 6px;
border-radius: 24px;
background: #fff;
box-shadow: var(--bc-shadow);
}
.hero {
flex: 999 1 560px;
min-width: 0;
padding: 40px;
border-radius: 36px;
background: var(--bc-pink);
display: flex;
flex-direction: column;
gap: 22px;
color: var(--bc-plum);
}
.hero__tag {
align-self: flex-start;
padding: 8px 14px;
border-radius: 999px;
background: #fff;
font-size: 14px;
font-weight: 600;
}
.hero__sub {
margin: 0;
font-size: 19px;
line-height: 1.5;
max-width: 560px;
}
.clean-promo {
margin-top: auto;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 18px 18px 18px 22px;
border-radius: 26px;
background: #fff;
color: var(--bc-ink);
text-decoration: none;
}
.clean-promo:hover {
color: var(--bc-ink);
}
.clean-promo__text {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
flex: 1 1 260px;
}
.clean-promo__text b {
font-family: var(--bc-font-head);
font-weight: 700;
font-size: 20px;
}
.clean-promo__text span {
font-size: 15px;
color: var(--bc-ink-2);
}
.clean-promo__cta {
flex: none;
}
.clean-promo:hover .clean-promo__cta {
opacity: 0.9;
}
.side {
flex: 1 1 380px;
min-width: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
.preview-row {
padding: 12px 14px;
border-radius: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.dir {
flex: 1 1 270px;
min-width: 0;
padding: 24px;
border-radius: 32px;
display: flex;
flex-direction: column;
gap: 12px;
}
.dir__title {
font-family: var(--bc-font-head);
font-weight: 800;
font-size: 26px;
line-height: 1;
}
.how {
flex: 999 1 640px;
min-width: 0;
padding: 28px;
border-radius: 36px;
background: var(--bc-dark);
color: #fff;
display: flex;
flex-direction: column;
gap: 18px;
}
.how__grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
.how__step {
padding: 16px;
border-radius: 24px;
background: var(--bc-dark-2);
display: flex;
flex-direction: column;
gap: 8px;
}
.how__num {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
}
.agency {
flex: 1 1 360px;
min-width: 0;
padding: 28px;
border-radius: 36px;
background: var(--bc-home);
color: var(--bc-home-ink);
display: flex;
align-items: center;
gap: 16px;
}
.sticker {
width: 120px;
height: 110px;
flex: none;
padding: 14px;
background: var(--bc-perks);
border-radius: 6px;
transform: rotate(6deg);
display: flex;
flex-direction: column;
justify-content: space-between;
color: var(--bc-plum);
}
.plan {
flex: 1 1 300px;
min-width: 0;
padding: 28px;
border-radius: 36px;
display: flex;
flex-direction: column;
gap: 14px;
}
.plan__list {
display: flex;
flex-direction: column;
gap: 8px;
font-size: 15px;
}
.plan__list div {
display: flex;
gap: 8px;
align-items: center;
}
.faq {
flex: 1 1 340px;
min-width: 0;
padding: 28px;
border-radius: 36px;
background: var(--bc-pink);
color: var(--bc-plum);
display: flex;
flex-direction: column;
gap: 8px;
}
.faq details {
border-radius: 20px;
background: #fff;
overflow: hidden;
color: var(--bc-ink);
}
.faq summary {
min-height: 48px;
padding: 12px 16px;
font-size: 15px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
cursor: pointer;
list-style: none;
}
.faq summary::-webkit-details-marker {
display: none;
}
.faq summary::after {
content: '+';
font-size: 18px;
}
.faq details[open] summary::after {
content: '−';
}
.faq details p {
margin: 0;
padding: 0 16px 14px;
font-size: 14px;
line-height: 1.45;
color: var(--bc-ink-2);
}
.footer {
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: space-between;
align-items: center;
padding: 18px 24px;
border-radius: 999px;
background: #fff;
font-size: 14px;
color: var(--bc-ink-2);
}
.footer__links {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.prose {
max-width: 760px;
margin: 0 auto;
padding: 32px 20px;
line-height: 1.6;
}
/* политика конфиденциальности */
.policy {
overflow-wrap: anywhere;
}
.policy h1 {
font-size: clamp(28px, 7vw, 44px);
text-wrap: balance;
}
.policy__top {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.policy__langs {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.policy__langs a {
padding: 4px 10px;
border-radius: 999px;
font-size: 13px;
font-weight: 600;
color: var(--bc-ink-2);
text-decoration: none;
}
.policy__langs a[aria-current='page'] {
background: var(--bc-plum);
color: #fff;
}
.policy__updated {
color: var(--bc-ink-2);
font-size: 14px;
}
.policy__toc {
margin: 20px 0 8px;
padding: 16px 20px 16px 40px;
border-radius: 24px;
background: #fff;
}
.policy__toc a {
color: inherit;
}
.policy__section {
scroll-margin-top: 16px;
}
.policy__section h2 {
margin: 32px 0 8px;
font-family: var(--bc-font-head);
font-size: 22px;
line-height: 1.25;
color: var(--bc-plum);
}
.policy__section ul {
padding-left: 22px;
}
.policy__section li {
margin-bottom: 6px;
}
.policy__footer {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 12px;
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid rgba(74, 16, 40, 0.15);
font-size: 14px;
color: var(--bc-ink-2);
}
.policy__footer a {
color: inherit;
}
.policy__legal {
display: flex;
flex-wrap: wrap;
gap: 8px 20px;
flex-basis: 100%;
}
/* страница цен */
.pr-plans {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin-top: 16px;
}
.pr-plan {
display: flex;
flex-direction: column;
gap: 8px;
padding: 18px;
border-radius: 20px;
background: #fff;
border: 1.5px solid rgba(74, 16, 40, 0.12);
}
.pr-plan[data-plan='plus'] {
background: var(--bc-pink);
border-color: var(--bc-pink);
}
.pr-plan[data-plan='elite'] {
background: var(--bc-perks);
border-color: var(--bc-perks);
}
.pr-plan h3 {
margin: 0;
font-family: var(--bc-font-head);
font-weight: 800;
font-style: italic;
font-size: 26px;
line-height: 1.1;
color: var(--bc-plum);
}
.pr-plan__price {
font-family: var(--bc-font-head);
font-weight: 600;
font-size: 18px;
color: var(--bc-plum);
}
.pr-plan ul {
display: flex;
flex-direction: column;
gap: 6px;
margin: 4px 0 0;
padding: 0;
list-style: none;
font-size: 15px;
line-height: 1.4;
}
.pr-plan li {
display: flex;
gap: 8px;
align-items: flex-start;
}
.pr-plan li svg {
flex: none;
margin-top: 2px;
color: var(--bc-plum);
}
.pr-plan small {
margin-top: auto;
padding-top: 8px;
font-size: 13px;
line-height: 1.4;
color: var(--bc-ink-2);
}
.pr-group h3 {
margin: 24px 0 4px;
font-family: var(--bc-font-head);
font-size: 18px;
color: var(--bc-plum);
}
.pr-list {
margin: 0;
border-radius: 20px;
background: #fff;
padding: 4px 18px;
}
.pr-row {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 16px;
padding: 12px 0;
}
.pr-row + .pr-row {
border-top: 1px solid rgba(74, 16, 40, 0.1);
}
.pr-row dt {
display: flex;
flex-direction: column;
gap: 2px;
font-weight: 600;
}
.pr-row dt a {
color: inherit;
}
.pr-row dt small {
font-weight: 400;
font-size: 13px;
line-height: 1.4;
color: var(--bc-ink-2);
}
.pr-row dd {
margin: 0;
flex: none;
font-family: var(--bc-font-head);
font-weight: 600;
white-space: nowrap;
color: var(--bc-plum);
}
@media (max-width: 720px) {
.pr-plans {
grid-template-columns: minmax(0, 1fr);
}
}
@media (max-width: 720px) {
.wrap {
padding: 12px 16px 24px;
}
.header {
border-radius: 28px;
padding: 12px 12px 12px 16px;
}
.nav {
display: none;
}
.header {
flex-wrap: nowrap;
padding: 8px 8px 8px 16px;
white-space: nowrap;
}
.header .langs {
flex-wrap: nowrap;
gap: 6px;
}
.header .langs > a {
margin-left: 0 !important;
}
.langs__row {
display: none;
}
.lang-menu {
display: block;
}
.tg-long {
display: none;
}
.tg-short {
display: inline;
}
.hero {
padding: 24px;
border-radius: 28px;
}
.clean-promo__cta {
width: 100%;
}
.how__grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.footer {
border-radius: 28px;
}
}
.bc-row-pill > span:last-child {
white-space: nowrap;
}
.perks-big {
font-family: var(--bc-font-head);
font-weight: 800;
font-size: clamp(40px, 4.2vw, 64px);
line-height: 1;
letter-spacing: -2px;
white-space: nowrap;
}
/* услуга ещё не запущена */
.bc-row-pill[data-soon='true'] > span:last-child {
font-size: 13px;
font-weight: 600;
opacity: 0.7;
}
a.bc-row-pill {
text-decoration: none;
color: inherit;
}
a.bc-row-pill:hover {
box-shadow: inset 0 0 0 2px currentColor;
color: inherit;
}
.langs-sticker {
display: flex;
flex-direction: column;
gap: 2px;
font-family: var(--bc-font-head);
font-weight: 700;
font-size: 20px;
line-height: 1.1;
}
.langs-sticker__list {
font-size: 15px;
font-weight: 600;
word-spacing: 4px;
white-space: nowrap;
}
+31
View File
@@ -0,0 +1,31 @@
import type { MetadataRoute } from 'next';
import { SITE } from '../lib/seo';
/**
* Всем поисковикам и ботам ИИ (ChatGPT, Perplexity, Claude, Gemini) — можно всё, кроме служебных адресов.
* Боты ИИ перечислены явно: так видно, что доступ им открыт намеренно.
*/
const AI_BOTS = [
'OAI-SearchBot',
'ChatGPT-User',
'GPTBot',
'PerplexityBot',
'Perplexity-User',
'Claude-SearchBot',
'Claude-User',
'ClaudeBot',
'Google-Extended',
'Applebot-Extended',
];
export default function robots(): MetadataRoute.Robots {
// API и файлы на том же домене (nginx проксирует их в barev-app) — в поиске не нужны
const disallow = ['/trpc/', '/files/', '/client-log'];
return {
rules: [
{ userAgent: '*', allow: '/', disallow },
{ userAgent: AI_BOTS, allow: '/', disallow },
],
sitemap: `${SITE}/sitemap.xml`,
};
}
+282
View File
@@ -0,0 +1,282 @@
/* Страницы услуг (/social-card, /residence-permit, /airport): тёмная тема главной, текст — колонкой для чтения */
.s-crumbs {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 8px 8px 0;
font-size: 14px;
color: #e9c9d5;
}
.s-crumbs a {
color: var(--bc-pink);
}
.s-hero {
display: grid;
grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
gap: 32px;
align-items: start;
padding: 24px 8px 8px;
}
.s-hero__text {
min-width: 0;
}
.s-title {
margin: 0;
font-family: var(--bc-font-head);
font-weight: 800;
font-size: clamp(34px, 5vw, 64px);
line-height: 1;
letter-spacing: -0.03em;
text-wrap: balance;
color: var(--bc-bg);
}
.s-lead {
margin: 20px 0 0;
max-width: 62ch;
font-size: 18px;
line-height: 1.6;
color: #f4dce5;
}
.s-updated {
margin: 14px 0 0;
font-size: 13px;
color: #d9b4c3;
}
/* карточка цены — как клубная карта */
.s-price {
display: flex;
flex-direction: column;
gap: 6px;
padding: 28px;
border-radius: 32px;
background: var(--bc-pink);
color: var(--bc-plum);
}
.s-price__label {
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.s-price__value {
font-family: var(--bc-font-head);
font-weight: 800;
font-style: italic;
font-size: clamp(36px, 4.4vw, 56px);
line-height: 1;
letter-spacing: -0.03em;
}
.s-price__note {
font-size: 15px;
line-height: 1.45;
}
.s-price__cta {
margin-top: 16px;
height: 56px;
background: var(--bc-plum);
color: #fff;
}
.s-price__cta:hover {
color: #fff;
opacity: 0.92;
}
.s-price__more {
align-self: center;
margin-top: 6px;
font-size: 15px;
font-weight: 600;
color: var(--bc-plum);
}
.s-facts dl {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin: 8px 0 0;
}
.s-fact {
padding: 18px 20px;
border-radius: 24px;
border: 1.5px solid rgba(246, 195, 211, 0.3);
}
.s-fact dt {
font-size: 13px;
color: #e9c9d5;
}
.s-fact dd {
margin: 6px 0 0;
font-family: var(--bc-font-head);
font-weight: 700;
font-size: 19px;
line-height: 1.25;
color: var(--bc-bg);
}
.s-section {
margin-top: 32px;
padding: 0 8px;
}
.s-h2 {
margin: 0 0 8px;
font-family: var(--bc-font-head);
font-weight: 800;
font-size: clamp(28px, 3.4vw, 44px);
line-height: 1.05;
letter-spacing: -0.02em;
text-wrap: balance;
}
.s-h3 {
margin: 0 0 10px;
font-family: var(--bc-font-head);
font-weight: 700;
font-size: 20px;
}
.s-steps .v2-step {
grid-template-columns: 110px minmax(0, 1fr);
}
.s-steps .v2-step__body h3 {
margin: 0;
font-family: var(--bc-font-head);
font-weight: 700;
font-size: clamp(20px, 2.2vw, 26px);
}
/* подробности — колонкой ~65 знаков */
.s-body {
display: flex;
flex-direction: column;
gap: 28px;
max-width: 760px;
margin-top: 40px;
padding: 0 8px;
}
.s-block {
font-size: 17px;
line-height: 1.65;
color: #f4dce5;
}
.s-block .s-h2 {
font-size: clamp(24px, 2.6vw, 32px);
color: var(--bc-bg);
}
.s-block p {
margin: 0 0 10px;
}
.s-block ul {
margin: 0 0 10px;
padding-left: 22px;
}
.s-block li {
margin-bottom: 6px;
}
.s-faq {
display: flex;
flex-direction: column;
max-width: 860px;
}
.s-faq details {
border-top: 1px solid rgba(246, 195, 211, 0.16);
padding: 18px 0;
}
.s-faq summary {
display: flex;
justify-content: space-between;
gap: 16px;
cursor: pointer;
list-style: none;
}
.s-faq summary::-webkit-details-marker {
display: none;
}
.s-faq summary::after {
content: '+';
flex: none;
font-family: var(--bc-font-head);
font-size: 26px;
line-height: 1;
color: var(--bc-pink);
}
.s-faq details[open] summary::after {
content: '−';
}
.s-faq summary h3 {
margin: 0;
font-family: var(--bc-font-head);
font-weight: 700;
font-size: clamp(18px, 2vw, 22px);
line-height: 1.3;
}
.s-faq details p {
margin: 10px 0 0;
max-width: 68ch;
font-size: 17px;
line-height: 1.6;
color: #f4dce5;
}
.s-meta {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 32px;
margin-top: 16px;
padding: 24px 8px 0;
}
.s-related {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 0;
padding: 0;
list-style: none;
}
.s-related a {
display: inline-block;
padding: 8px 16px;
border-radius: 999px;
border: 1.5px solid rgba(246, 195, 211, 0.5);
color: var(--bc-bg);
text-decoration: none;
font-weight: 600;
font-size: 15px;
}
.s-related a:hover {
border-color: var(--bc-pink);
color: var(--bc-pink);
}
@media (max-width: 900px) {
.s-hero,
.s-meta {
grid-template-columns: minmax(0, 1fr);
}
.s-facts dl {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 720px) {
.s-hero {
padding: 16px 0 0;
}
.s-section,
.s-body,
.s-meta,
.s-crumbs {
padding-left: 0;
padding-right: 0;
}
.s-price {
padding: 22px;
border-radius: 28px;
}
.s-steps .v2-step {
grid-template-columns: minmax(0, 1fr);
}
}
@media (max-width: 380px) {
.s-facts dl {
grid-template-columns: minmax(0, 1fr);
}
}
+15
View File
@@ -0,0 +1,15 @@
import { LOCALES } from '@barev/i18n';
import type { MetadataRoute } from 'next';
import { INDEXED_PATHS, languageAlternates, SITE } from '../lib/seo';
/** Все индексируемые страницы на 5 языках; у каждой — hreflang на все версии, включая саму себя */
export default function sitemap(): MetadataRoute.Sitemap {
return INDEXED_PATHS.flatMap((path) =>
LOCALES.map((locale) => ({
url: `${SITE}/${locale}${path}`,
changeFrequency: 'weekly' as const,
priority: path === '' ? 1 : 0.8,
alternates: { languages: languageAlternates(path, true) },
})),
);
}
File diff suppressed because it is too large. Load diff