Stage 6 (part 2): rest of the client app in the new style, promo code in the order form

Profile with club card, plum theme for buttons/selections and the sticky total,
move screens with the full-width hero, services/docs headers, empty docs fix.
Promo code field: applies to the quote, shows why a code is rejected,
says when the membership discount is bigger.

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-21 04:08:47 +04:00
1 parent ff527ad6d2
commit bbb25d3d0d
12 files changed
+371 -85

No files matched your search

+120
View File
@@ -827,6 +827,126 @@
color: #fff;
}
/* ───── общий вид: слива вместо чёрного ───── */
.bc-btn--dark,
.bc-chip[aria-pressed='true'],
.day[aria-pressed='true'],
.slot[aria-pressed='true'] {
background: var(--bc-plum);
color: #fff;
}
.option[aria-pressed='true'] {
border-color: var(--bc-plum);
background: var(--bc-pink);
color: var(--bc-plum);
}
.extra-toggle[aria-pressed='true'] {
background: var(--green);
border-color: var(--green);
color: var(--green-ink);
}
.bc-input:focus {
border-color: var(--bc-plum);
}
.app .bc-card {
border-radius: 26px;
}
.page-head {
padding-top: 4px;
}
.page-head .bc-h2 {
font-weight: 800;
font-size: 28px;
}
/* закреплённый итог в оформлении заказа — сливовый, кнопка розовая */
.app .sticky-cta {
background: var(--bc-plum);
color: #fff;
box-shadow: 0 12px 30px rgba(74, 16, 40, 0.35) !important;
}
.app .sticky-cta .bc-muted {
color: rgba(255, 255, 255, 0.72);
}
.app .sticky-cta .bc-btn {
background: var(--bc-pink);
color: var(--bc-plum);
}
/* верх переезда: блок плана внутри сливового верха */
.hero.move-hero {
gap: 6px;
}
.hero.move-hero .hero__hello {
margin-bottom: 10px;
}
/* промокод */
.promo-form {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
}
.promo-form .pill {
min-height: 50px;
}
.promo-form__error {
grid-column: 1 / -1;
}
.promo-applied {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 10px 10px 10px 16px;
border-radius: 18px;
background: var(--bc-perks);
color: var(--bc-perks-ink);
}
.promo-applied > span {
display: flex;
flex-direction: column;
}
.promo-applied b {
font-family: var(--bc-font-head);
letter-spacing: 0.04em;
}
.promo-applied button {
border: 0;
background: rgba(255, 255, 255, 0.6);
color: inherit;
font: inherit;
font-weight: 600;
font-size: 14px;
padding: 8px 14px;
border-radius: var(--bc-pill);
cursor: pointer;
}
/* ссылки в профиле */
.links {
display: flex;
flex-direction: column;
padding: 4px 16px;
border-radius: 26px;
background: var(--bc-surface);
}
.links a {
display: flex;
align-items: center;
gap: 12px;
min-height: 54px;
border-top: 1px solid var(--bc-line);
color: var(--bc-ink);
text-decoration: none;
font-weight: 600;
}
.links a:first-child {
border-top: 0;
}
.links a span {
flex: 1;
}
/* ───── нижнее меню — слива и розовая кнопка чата ───── */
.bc-bottomnav {
background: var(--bc-plum);
+65 -33
View File
@@ -1,7 +1,14 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Card, Center, Icon, Spinner } from '@barev/ui';
import { errorText, haptic, navigate, useI18n, useToast, useTRPC } from '@barev/web';
import { itemHint, itemName, PageHead } from '../lib.tsx';
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Card, Center, Icon, Spinner } from "@barev/ui";
import {
errorText,
haptic,
navigate,
useI18n,
useToast,
useTRPC,
} from "@barev/web";
import { itemHint, itemName } from "../lib.tsx";
/** Чек-лист документов, которые лучше собрать до переезда; отметки сохраняются в заявке на переезд */
export function Docs() {
@@ -14,8 +21,11 @@ export function Docs() {
const toggle = useMutation(
trpc.client.relocation.toggleDoc.mutationOptions({
onMutate: () => haptic.tap(),
onSuccess: () => void qc.invalidateQueries({ queryKey: trpc.client.relocation.get.queryKey() }),
onError: (e) => toast(errorText(t, e), 'error'),
onSuccess: () =>
void qc.invalidateQueries({
queryKey: trpc.client.relocation.get.queryKey(),
}),
onError: (e) => toast(errorText(t, e), "error"),
}),
);
if (!plan.data)
@@ -28,41 +38,63 @@ export function Docs() {
return (
<main className="app">
<PageHead title={t('client.docs.title')} back={false} />
<p className="bc-muted" style={{ margin: 0 }}>
{t('client.docs.sub')}
<header className="screen-head">
<h1 className="screen-head__title">{t("client.docs.title")}</h1>
</header>
<p className="bc-muted" style={{ margin: "-6px 0 0" }}>
{t("client.docs.sub")}
</p>
{!lead && (
<Card tone="perks" tight>
<a href="#/" className="bc-small" style={{ fontWeight: 600, color: 'inherit' }}>
{t('client.docs.needPlan')}
<a
href="#/"
className="bc-small"
style={{ fontWeight: 600, color: "inherit" }}
>
{t("client.docs.needPlan")}
</a>
</Card>
)}
<Card style={{ padding: 8 }}>
<ul className="docs">
{(lead?.docs ?? []).map((d) => {
const hint = itemHint(plan.data.names, 'DOC', d, locale);
return (
<li key={d.id}>
<button className="doc" aria-pressed={d.checked} disabled={toggle.isPending} onClick={() => toggle.mutate({ docId: d.id, checked: !d.checked })}>
<span className="doc__box" aria-hidden="true">
{d.checked && <Icon name="check" size={16} strokeWidth={3} />}
</span>
<span>
<b>{itemName(plan.data.names, 'DOC', d, locale)}</b>
{hint && <span className="bc-small bc-muted">{hint}</span>}
</span>
</button>
</li>
);
})}
</ul>
</Card>
{lead && lead.docs.length > 0 && (
<Card style={{ padding: 8 }}>
<ul className="docs">
{(lead?.docs ?? []).map((d) => {
const hint = itemHint(plan.data.names, "DOC", d, locale);
return (
<li key={d.id}>
<button
className="doc"
aria-pressed={d.checked}
disabled={toggle.isPending}
onClick={() =>
toggle.mutate({ docId: d.id, checked: !d.checked })
}
>
<span className="doc__box" aria-hidden="true">
{d.checked && (
<Icon name="check" size={16} strokeWidth={3} />
)}
</span>
<span>
<b>{itemName(plan.data.names, "DOC", d, locale)}</b>
{hint && (
<span className="bc-small bc-muted">{hint}</span>
)}
</span>
</button>
</li>
);
})}
</ul>
</Card>
)}
{me.data && (
<button className="bc-btn bc-btn--light bc-btn--block" onClick={() => navigate('/chat')}>
<button
className="bc-btn bc-btn--light bc-btn--block"
onClick={() => navigate("/chat")}
>
<Icon name="chat" size={18} />
{t('client.docs.ask')}
{t("client.docs.ask")}
</button>
)}
</main>
+12 -10
View File
@@ -31,16 +31,18 @@ export function Move() {
const days = lead.moveDate ? Math.ceil((new Date(lead.moveDate).getTime() - Date.now()) / DAY) : null;
return (
<main className="app">
<header className="bc-between" style={{ alignItems: 'flex-start' }}>
<div>
<div className="bc-small bc-muted">{t('client.home.hello')}</div>
<h1 className="bc-h2">{me.data.firstName ?? t('client.home.guest')}</h1>
<main className="app app--hero">
<section className="hero move-hero">
<div className="hero__word" aria-hidden="true">
Barev
</div>
<div className="bc-between" style={{ alignItems: 'flex-start', position: 'relative' }}>
<h1 className="hero__hello">
<small>{t('client.home.hello')}</small>
<span>{me.data.firstName ?? t('client.home.guest')}</span>
</h1>
<LevelBadge level={me.data.membership.level as Level} />
</div>
<LevelBadge level={me.data.membership.level as Level} />
</header>
<section className="move-hero">
<div className="move-hero__label">{t('client.move.title')}</div>
<div className="move-hero__when">
{lead.moveWhen === 'date' && lead.moveDate
@@ -55,7 +57,7 @@ export function Move() {
</section>
<Card style={{ padding: '18px 18px 8px' }}>
<h2 className="bc-h3" style={{ marginBottom: 10 }}>
<h2 className="panel__title">
{t(`landing.quiz.package_${lead.package}`)}
</h2>
{steps.length === 0 ? (
+2 -2
View File
@@ -66,8 +66,8 @@ export function MoveForm() {
);
return (
<main className="app">
<section className="move-hero">
<main className="app app--hero">
<section className="hero move-hero">
<div className="move-hero__label">{me.data?.firstName ? t('client.move.formHello', { name: me.data.firstName }) : t('client.move.title')}</div>
<div className="move-hero__when">{t('client.move.formTitle')}</div>
<div className="move-hero__progress">{t('client.move.formSub')}</div>
+66 -2
View File
@@ -27,6 +27,10 @@ export function OrderForm({ code }: { code: string }) {
const [address, setAddress] = useState('');
const [note, setNote] = useState('');
const [methodId, setMethodId] = useState('');
// промокод: что ввёл клиент и что применено к расчёту
const [promoInput, setPromoInput] = useState('');
const [promo, setPromo] = useState('');
const [promoError, setPromoError] = useState<string | null>(null);
const [touched, setTouched] = useState(false);
// дефолты услуги, затем — параметры повторяемого заказа (ORD-7)
@@ -50,10 +54,18 @@ export function OrderForm({ code }: { code: string }) {
}, [days, day]);
const quote = useQuery({
...trpc.client.quote.queryOptions({ serviceCode: code, params: params ?? {} }),
...trpc.client.quote.queryOptions({ serviceCode: code, params: params ?? {}, promoCode: promo || undefined }),
enabled: !!params,
placeholderData: keepPreviousData,
retry: false,
});
// неподходящий код — показываем причину и считаем без него
useEffect(() => {
if (promo && quote.error) {
setPromoError(errorText(t, quote.error));
setPromo('');
}
}, [promo, quote.error, t]);
const slots = useQuery({
...trpc.client.slots.queryOptions({ serviceCode: code, params: params ?? {}, dateKey: day ?? '' }),
enabled: !!params && !!day,
@@ -119,6 +131,7 @@ export function OrderForm({ code }: { code: string }) {
note: note.trim() || undefined,
paymentMethodId: methodId || undefined,
repeatOfId: repeatId ?? undefined,
promoCode: q?.promo ? promo : undefined,
});
}
@@ -267,7 +280,7 @@ export function OrderForm({ code }: { code: string }) {
</div>
{q.discountAmount > 0 ? (
<div className="bc-between" style={{ color: 'var(--bc-accent)', fontWeight: 600 }}>
<span>{t('client.order.discount', { pct: q.discountPct })}</span>
<span>{q.promo ? t('client.order.promoLine', { code: q.promo.code, pct: q.discountPct }) : t('client.order.discount', { pct: q.discountPct })}</span>
<span>−{amd(q.discountAmount)}</span>
</div>
) : (
@@ -281,6 +294,25 @@ export function OrderForm({ code }: { code: string }) {
) : (
<Spinner />
)}
<PromoField
value={promoInput}
onChange={(v) => {
setPromoInput(v);
setPromoError(null);
}}
applied={promo}
active={!!q?.promo}
error={promoError}
loading={quote.isFetching}
onApply={() => {
setPromoError(null);
setPromo(promoInput.trim());
}}
onRemove={() => {
setPromo('');
setPromoInput('');
}}
/>
<p className="bc-small bc-muted" style={{ margin: 0 }}>
{t('client.order.cancelPolicy', { hours: me.data?.freeCancelHours ?? 24, pct: s.lateCancelFeePct })}
</p>
@@ -414,3 +446,35 @@ function ExtrasPicker({
</Card>
);
}
/** Промокод: применённый — плашкой с кнопкой «Убрать»; если скидка подписки больше — так и пишем */
function PromoField(p: { value: string; onChange: (v: string) => void; applied: string; active: boolean; error: string | null; loading: boolean; onApply: () => void; onRemove: () => void }) {
const { t } = useI18n();
if (p.applied && !p.loading)
return (
<div className="promo-applied">
<span>
<b>{p.applied.toUpperCase()}</b>
{!p.active && <span className="bc-small">{t('client.order.promoWeaker')}</span>}
</span>
<button type="button" onClick={p.onRemove}>
{t('client.order.promoRemove')}
</button>
</div>
);
return (
<form
className="promo-form"
onSubmit={(e) => {
e.preventDefault();
if (p.value.trim().length >= 2) p.onApply();
}}
>
<input className="bc-input" value={p.value} maxLength={40} autoCapitalize="characters" placeholder={t('client.order.promoPlaceholder')} aria-label={t('client.order.promo')} onChange={(e) => p.onChange(e.target.value.toUpperCase())} />
<button type="submit" className="pill pill--dark" disabled={p.value.trim().length < 2 || p.loading}>
{t('client.order.promoApply')}
</button>
{p.error && <div className="bc-error promo-form__error">{p.error}</div>}
</form>
);
}
+36 -20
View File
@@ -3,7 +3,7 @@ import { LOCALE_NAMES, LOCALES, type Locale } from '@barev/i18n';
import { Button, Card, Chip, Field, Icon, Segmented } from '@barev/ui';
import { errorText, haptic, useI18n, useLocation, useToast, useTRPC } from '@barev/web';
import { useState } from 'react';
import { LEVEL_NAME, LevelBadge, PageHead, type Level } from '../lib.tsx';
import { ClubTicket, LEVEL_NAME, type Level } from '../lib.tsx';
export function Profile() {
const { t, dt } = useI18n();
@@ -14,21 +14,26 @@ export function Profile() {
const plus = level !== 'BAREV';
return (
<main className="app">
<PageHead title={t('client.profile.title')} back={false} />
<Card className="bc-stack">
<div className="bc-between">
<div>
<div className="bc-h3">{me.data?.firstName}</div>
<div className="bc-small bc-muted">{me.data?.agencyName ? t('client.profile.fromAgency', { agency: me.data.agencyName }) : t('client.profile.noAgency')}</div>
</div>
<LevelBadge level={level} />
</div>
{plus && me.data?.membership.endsAt && <div className="bc-small">{t('client.home.levelUntil', { plan: LEVEL_NAME[level], date: dt(me.data.membership.endsAt, { day: 'numeric', month: 'long', year: 'numeric' }) })}</div>}
<a href="#/membership" className="bc-row" style={{ fontWeight: 600, color: 'var(--bc-accent)', textDecoration: 'none' }}>
{t('client.profile.membership')} <Icon name="arrow" size={16} />
</a>
</Card>
<main className="app app--hero">
<section className="hero">
<h1 className="hero__hello">
<small>{t('client.profile.title')}</small>
<span>{me.data?.firstName}</span>
</h1>
<p className="hero__note">{me.data?.agencyName ? t('client.profile.fromAgency', { agency: me.data.agencyName }) : t('client.profile.noAgency')}</p>
<ClubTicket
level={level}
code={me.data?.cardCode}
href="#/membership"
until={plus && me.data?.membership.endsAt ? t('client.home.levelUntil', { plan: LEVEL_NAME[level], date: dt(me.data.membership.endsAt, { day: 'numeric', month: 'long', year: 'numeric' }) }) : t('client.home.free')}
action={
<>
<Icon name="star" size={26} />
<span>{t('client.card.plans')}</span>
</>
}
/>
</section>
<SegmentSwitch current={me.data?.segment ?? null} />
@@ -36,12 +41,23 @@ export function Profile() {
<LanguagePicker />
<Card tight>
<a href="#/orders" className="bc-between" style={{ textDecoration: 'none', minHeight: 44 }}>
<span style={{ fontWeight: 600 }}>{t('client.orders.title')}</span>
<nav className="links">
<a href="#/orders">
<Icon name="clock" size={20} />
<span>{t('client.orders.title')}</span>
<Icon name="arrow" size={18} />
</a>
</Card>
<a href="#/perks">
<Icon name="gift" size={20} />
<span>{t('client.card.title')}</span>
<Icon name="arrow" size={18} />
</a>
<a href="#/chat">
<Icon name="chat" size={20} />
<span>{t('client.nav.chat')}</span>
<Icon name="arrow" size={18} />
</a>
</nav>
</main>
);
}
+4 -2
View File
@@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query';
import { CATEGORY_ICON, Center, Icon, Spinner, type CategoryCode } from '@barev/ui';
import { useI18n, useLocation, useTRPC } from '@barev/web';
import { useEffect } from 'react';
import { levelDiscount, PageHead, toneClass, type Level } from '../lib.tsx';
import { levelDiscount, toneClass, type Level } from '../lib.tsx';
/** Каталог: направления со скидкой по уровню клиента; услуги без онлайн-записи — заявка «перезвоните» */
export function Services() {
@@ -25,7 +25,9 @@ export function Services() {
const level = catalog.data.level as Level;
return (
<main className="app">
<PageHead title={t('client.services.title')} back={false} />
<header className="screen-head">
<h1 className="screen-head__title">{t('client.services.title')}</h1>
</header>
{catalog.data.categories
.filter((c) => c.code !== 'PERKS')
.map((c) => (
+12 -2
View File
@@ -444,7 +444,11 @@
"OFFER_NOT_AVAILABLE": "This offer isn’t available at the client’s level.",
"OFFER_LIMIT_REACHED": "The limit for this offer is already used up.",
"CONCIERGE_SUBSCRIBERS_ONLY": "The personal concierge is for Barev+ and Barev Elite members. For now, write to support — we will help too.",
"ASSIGNEE_NO_ACCESS": "This team member has no access to this channel."
"ASSIGNEE_NO_ACCESS": "This team member has no access to this channel.",
"PROMO_INVALID": "This promo code doesn't exist",
"PROMO_EXPIRED": "This promo code is no longer valid",
"PROMO_USED": "This promo code has already been used",
"PROMO_NOT_FOR_SERVICE": "This promo code doesn't apply to this service"
},
"client": {
"nav": {
@@ -525,7 +529,13 @@
"cancelPolicy": "Free cancellation and rescheduling up to {{hours}} h before. Later — a {{pct}}% fee.",
"created": "Order #{{number}} created",
"extras": "Extras",
"add": "Add"
"add": "Add",
"promo": "Promo code",
"promoPlaceholder": "Promo code, if you have one",
"promoApply": "Apply",
"promoRemove": "Remove",
"promoLine": "Promo {{code}} −{{pct}}%",
"promoWeaker": "Your membership discount is bigger, so it applies"
},
"orders": {
"title": "My orders",
+12 -2
View File
@@ -444,7 +444,11 @@
"OFFER_NOT_AVAILABLE": "Esta oferta no está disponible en el nivel del cliente.",
"OFFER_LIMIT_REACHED": "Ya se agotó el límite de esta oferta.",
"CONCIERGE_SUBSCRIBERS_ONLY": "El conserje personal es para miembros de Barev+ y Barev Elite. De momento, escribe a soporte: también te ayudaremos.",
"ASSIGNEE_NO_ACCESS": "Esta persona del equipo no tiene acceso a este canal."
"ASSIGNEE_NO_ACCESS": "Esta persona del equipo no tiene acceso a este canal.",
"PROMO_INVALID": "Este código no existe",
"PROMO_EXPIRED": "El código ya no es válido",
"PROMO_USED": "Este código ya se ha usado",
"PROMO_NOT_FOR_SERVICE": "El código no es válido para este servicio"
},
"client": {
"nav": {
@@ -525,7 +529,13 @@
"cancelPolicy": "Cancelación y cambio gratis hasta {{hours}} h antes. Después, se retiene un {{pct}}%.",
"created": "Pedido n.º {{number}} creado",
"extras": "Extras",
"add": "Añadir"
"add": "Añadir",
"promo": "Código promocional",
"promoPlaceholder": "Código promocional, si tienes",
"promoApply": "Aplicar",
"promoRemove": "Quitar",
"promoLine": "Código {{code}} −{{pct}}%",
"promoWeaker": "El descuento de tu suscripción es mayor y se aplica ese"
},
"orders": {
"title": "Mis pedidos",
+15 -5
View File
@@ -444,7 +444,11 @@
"OFFER_NOT_AVAILABLE": "Այս առաջարկը հասանելի չէ հաճախորդի մակարդակում։",
"OFFER_LIMIT_REACHED": "Այս առաջարկի սահմանաչափն արդեն սպառված է։",
"CONCIERGE_SUBSCRIBERS_ONLY": "Անձնական կոնսիերժը Barev+ և Barev Elite բաժանորդների համար է։ Առայժմ գրեք աջակցությանը՝ մենք էլ կօգնենք։",
"ASSIGNEE_NO_ACCESS": "Այս աշխատակիցը մուտք չունի այս ալիքին։"
"ASSIGNEE_NO_ACCESS": "Այս աշխատակիցը մուտք չունի այս ալիքին։",
"PROMO_INVALID": "Նման պրոմոկոդ չկա",
"PROMO_EXPIRED": "Պրոմոկոդն այլևս չի գործում",
"PROMO_USED": "Այս պրոմոկոդն արդեն օգտագործված է",
"PROMO_NOT_FOR_SERVICE": "Պրոմոկոդը չի գործում այս ծառայության համար"
},
"client": {
"nav": {
@@ -490,7 +494,7 @@
"cardShow": "Ցույց տալ QR",
"free": "Անվճար մակարդակ",
"chatTitle": "Ցանկացած հարց՝ չատում",
"chatText": "Կպատասխանենք ձեր լեզվով"
"chatText": "Կպատասխանենք քո լեզվով"
},
"services": {
"title": "Ծառայություններ",
@@ -525,7 +529,13 @@
"cancelPolicy": "Անվճար չեղարկում և տեղափոխում՝ ոչ ուշ, քան {{hours}} ժ առաջ։ Ավելի ուշ՝ պահվում է {{pct}}%։",
"created": "Պատվեր #{{number}}-ը ստեղծված է",
"extras": "Լրացուցիչ",
"add": "Ավելացնել"
"add": "Ավելացնել",
"promo": "Պրոմոկոդ",
"promoPlaceholder": "Պրոմոկոդ, եթե ունես",
"promoApply": "Կիրառել",
"promoRemove": "Հեռացնել",
"promoLine": "Պրոմոկոդ {{code}} −{{pct}}%",
"promoWeaker": "Բաժանորդագրության զեղչն ավելի մեծ է, գործում է այն"
},
"orders": {
"title": "Իմ հայտերը",
@@ -693,10 +703,10 @@
"comment": "Մեկնաբանություն՝ ըստ ցանկության",
"send": "Գնահատել",
"thanks": "Շնորհակալություն գնահատականի համար",
"yours": "Ձեր գնահատականը",
"yours": "Քո գնահատականը",
"change": "Փոխել",
"star": "{{count}}՝ 5-ից",
"prompt": "Գնահատեք պատվերը"
"prompt": "Գնահատիր պատվերը"
},
"card": {
"title": "Ակումբային քարտ",
+15 -5
View File
@@ -444,7 +444,11 @@
"OFFER_NOT_AVAILABLE": "Это предложение недоступно на уровне клиента.",
"OFFER_LIMIT_REACHED": "Лимит по этому предложению уже исчерпан.",
"CONCIERGE_SUBSCRIBERS_ONLY": "Личный консьерж — для подписчиков Barev+ и Barev Elite. Пока пишите в поддержку — мы тоже поможем.",
"ASSIGNEE_NO_ACCESS": "У этого сотрудника нет доступа к этому каналу."
"ASSIGNEE_NO_ACCESS": "У этого сотрудника нет доступа к этому каналу.",
"PROMO_INVALID": "Такого промокода нет",
"PROMO_EXPIRED": "Промокод больше не действует",
"PROMO_USED": "Этот промокод уже использован",
"PROMO_NOT_FOR_SERVICE": "Промокод не действует на эту услугу"
},
"client": {
"nav": {
@@ -490,7 +494,7 @@
"cardShow": "Показать QR",
"free": "Бесплатный уровень",
"chatTitle": "Любой вопрос — в чат",
"chatText": "Ответим на вашем языке"
"chatText": "Ответим на твоём языке"
},
"services": {
"title": "Услуги",
@@ -525,7 +529,13 @@
"cancelPolicy": "Бесплатная отмена и перенос — не позднее чем за {{hours}} ч. Позже — удержание {{pct}}%.",
"created": "Заказ #{{number}} создан",
"extras": "Дополнительно",
"add": "Добавить"
"add": "Добавить",
"promo": "Промокод",
"promoPlaceholder": "Промокод, если есть",
"promoApply": "Применить",
"promoRemove": "Убрать",
"promoLine": "Промокод {{code}} −{{pct}}%",
"promoWeaker": "Скидка по подписке больше — действует она"
},
"orders": {
"title": "Мои заявки",
@@ -693,10 +703,10 @@
"comment": "Комментарий — по желанию",
"send": "Оценить",
"thanks": "Спасибо за оценку!",
"yours": "Ваша оценка",
"yours": "Твоя оценка",
"change": "Изменить",
"star": "{{count}} из 5",
"prompt": "Оцените заказ"
"prompt": "Оцени заказ"
},
"card": {
"title": "Клубная карта",
+12 -2
View File
@@ -444,7 +444,11 @@
"OFFER_NOT_AVAILABLE": "该优惠不适用于客户当前等级。",
"OFFER_LIMIT_REACHED": "该优惠次数已用完。",
"CONCIERGE_SUBSCRIBERS_ONLY": "专属礼宾仅面向 Barev+ 和 Barev Elite 会员。目前请联系客服,我们同样会帮你。",
"ASSIGNEE_NO_ACCESS": "该成员无权访问此频道。"
"ASSIGNEE_NO_ACCESS": "该成员无权访问此频道。",
"PROMO_INVALID": "优惠码不存在",
"PROMO_EXPIRED": "优惠码已失效",
"PROMO_USED": "该优惠码已使用",
"PROMO_NOT_FOR_SERVICE": "该优惠码不适用于此服务"
},
"client": {
"nav": {
@@ -525,7 +529,13 @@
"cancelPolicy": "开始前 {{hours}} 小时以上可免费取消或改期,之后取消将扣除 {{pct}}%。",
"created": "订单 #{{number}} 已创建",
"extras": "附加服务",
"add": "添加"
"add": "添加",
"promo": "优惠码",
"promoPlaceholder": "如有优惠码请输入",
"promoApply": "使用",
"promoRemove": "移除",
"promoLine": "优惠码 {{code}} −{{pct}}%",
"promoWeaker": "会员折扣更大,将使用会员折扣"
},
"orders": {
"title": "我的订单",