Bariki cashback, order edit, and current work in progress

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:57:03 +04:00
1 parent dda917fc71
commit 45477fb018
94 files changed
+10955 -650

No files matched your search

+2 -1
View File
@@ -5,6 +5,7 @@ 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';
@@ -113,7 +114,7 @@ export default async function CleanPage({ params }: { params: Promise<{ locale:
<div className="footer__links">
<a href={`/${locale}`}>{t('landing.clean.home')}</a>
<FooterContacts />
<a href={`/${locale}/privacy`}>{t('landing.footer.privacy')}</a>
<LegalLinks locale={locale} />
</div>
</footer>
</div>
+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>
);
}
+10 -68
View File
@@ -1,80 +1,22 @@
import { createT, isLocale, LOCALES, type Locale } from '@barev/i18n';
import { Logo } from '@barev/ui';
import type { Metadata } from 'next';
import { isLocale, LOCALES } from '@barev/i18n';
import { notFound } from 'next/navigation';
import { RichText } from '../../../components/rich-text';
import { FooterContacts, LegalLine } from '../../../components/seo';
import { CONTACTS, languageAlternates } from '../../../lib/seo';
import { LegalDocument, legalMetadata } from '../../../components/legal';
export const dynamicParams = false;
export function generateStaticParams() {
return LOCALES.map((locale) => ({ locale }));
}
/** Разделы политики по порядку; тексты — privacy.<id>.title / privacy.<id>.body */
/** Разделы по порядку; тексты — privacy.<id>.title / privacy.<id>.body */
const SECTIONS = ['operator', 'data', 'purposes', 'basis', 'recipients', 'location', 'retention', 'security', 'rights', 'marketing', 'cookies', 'age', 'changes'] as const;
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('privacy.title')} — Barev Club`,
description: t('privacy.intro'),
alternates: { canonical: `/${locale}/privacy`, languages: languageAlternates('/privacy') },
// своё превью ссылки, иначе подставляется старый слоган из общего layout
openGraph: { title: `${t('privacy.title')} — Barev Club`, description: t('privacy.intro'), locale, type: 'article', siteName: 'Barev Club' },
twitter: { card: 'summary_large_image', title: `${t('privacy.title')} — Barev Club`, description: t('privacy.intro') },
};
export function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
return legalMetadata(params, 'privacy');
}
/** ONB-3: политика конфиденциальности. Оператор — ИП владельца; основной текст на русском, переводы needs_review */
export default async function Privacy({ params }: { params: Promise<{ locale: string }> }) {
const { locale: raw } = await params;
if (!isLocale(raw)) notFound();
const locale: Locale = raw;
const t = createT(locale);
const vars = { email: CONTACTS.email, phone: CONTACTS.phoneDisplay, tin: CONTACTS.tin };
return (
<main className="prose policy">
<div className="policy__top">
<a href={`/${locale}`} style={{ textDecoration: 'none' }}>
<Logo />
</a>
<nav className="policy__langs" aria-label="Language">
{LOCALES.map((l) => (
<a key={l} href={`/${l}/privacy`} hrefLang={l} aria-current={l === locale ? 'page' : undefined}>
{l.toUpperCase()}
</a>
))}
</nav>
</div>
<h1 className="bc-h2" style={{ marginTop: 24 }}>
{t('privacy.title')}
</h1>
<p className="policy__updated">{t('privacy.updated')}</p>
<p>{t('privacy.intro')}</p>
<ol className="policy__toc">
{SECTIONS.map((s) => (
<li key={s}>
<a href={`#${s}`}>{t(`privacy.${s}.title`)}</a>
</li>
))}
</ol>
{SECTIONS.map((s, i) => (
<section key={s} id={s} className="policy__section">
<h2>
{i + 1}. {t(`privacy.${s}.title`)}
</h2>
<RichText text={t(`privacy.${s}.body`, vars)} />
</section>
))}
<footer className="policy__footer">
<LegalLine locale={locale} />
<span className="footer__links">
<FooterContacts />
</span>
</footer>
</main>
);
/** 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} />;
}
+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} />;
}
+33
View File
@@ -455,6 +455,33 @@ button.cl-extra {
color: #e9c9d5;
}
/* телефон сразу под комнатами и санузлами — единственное обязательное поле, кроме параметров */
.cl-hero-phone {
max-width: 420px;
margin-top: 4px;
}
.cl-hero-phone .bc-input {
background: #fff;
}
.cl-hero-phone[data-error='true'] .bc-input {
border-color: var(--bc-danger);
}
/* согласие на обработку данных — нажатием кнопки */
.cl-consent-note {
margin: 0;
font-size: 12px;
line-height: 1.45;
color: #e9c9d5;
}
.cl-consent-note a {
color: inherit;
}
.cl-consent-note--mobile {
display: none;
color: var(--bc-ink-2, #6b5d57);
padding: 0 4px;
}
/* нижняя панель — только на телефоне */
.cl-bar {
display: none;
@@ -659,6 +686,12 @@ button.cl-extra {
.cl-receipt .cl-btn-pink {
display: none;
}
.cl-receipt .cl-consent-note {
display: none;
}
.cl-consent-note--mobile {
display: block;
}
.cl-bar {
position: fixed;
left: 12px;
+123
View File
@@ -419,6 +419,129 @@ a:hover {
.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;