Snapshot of uncommitted work: landing, bots, prod infra, partner cabinet, backoffice stage 1
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
45d7512d2b
commit
d452824c62
314 files changed
+44394
-121
No files matched your search
@@ -0,0 +1,86 @@
|
||||
'use client';
|
||||
|
||||
import { discountFor, quote, type Locale } from '@barev/core';
|
||||
import { formatAmd, tFromMessages, type Messages } from '@barev/i18n';
|
||||
import { Stepper } from '@barev/ui';
|
||||
import { useMemo, useState } from 'react';
|
||||
import type { CleaningConfig } from '../lib/data';
|
||||
|
||||
const TYPES = ['regular', 'general', 'renovation'] as const;
|
||||
|
||||
/**
|
||||
* Калькулятор экономии с Barev+: цена уборки — функцией quote, как на сервере, скидка — как у участника Barev+.
|
||||
* Подписка вычитается из экономии; если выгоды нет, так и пишем и показываем, с какого числа уборок она появится.
|
||||
*/
|
||||
export function Savings({ locale, config, monthlyPrice, messages }: { locale: Locale; config: CleaningConfig; monthlyPrice: number; messages: Messages }) {
|
||||
const t = useMemo(() => tFromMessages(locale, messages), [locale, messages]);
|
||||
const amd = (n: number) => formatAmd(locale, n);
|
||||
// стартовый сценарий: генеральная уборка трёшки дважды в месяц — дальше человек подставляет своё
|
||||
const [type, setType] = useState<(typeof TYPES)[number]>('general');
|
||||
const [rooms, setRooms] = useState(3);
|
||||
const [count, setCount] = useState(2);
|
||||
|
||||
const pct = discountFor('BAREV_PLUS', config.categoryDiscountPct);
|
||||
const params = { cleaningType: type, rooms, bathrooms: 1 };
|
||||
const one = quote(config.pricing, params);
|
||||
const oneWithPlus = quote(config.pricing, params, pct);
|
||||
const without = one.totalPrice * count;
|
||||
const withPlus = oneWithPlus.totalPrice * count + monthlyPrice;
|
||||
const save = without - withPlus;
|
||||
const perCleaning = one.totalPrice - oneWithPlus.totalPrice;
|
||||
// выгода есть, когда скидка за n уборок больше цены подписки: n × perCleaning > monthlyPrice
|
||||
const breakeven = perCleaning > 0 ? Math.floor(monthlyPrice / perCleaning) + 1 : null;
|
||||
const max = Math.max(without, withPlus);
|
||||
|
||||
return (
|
||||
<div className="v5-calc">
|
||||
<div className="v5-inputs">
|
||||
<div className="v5-seg" role="radiogroup" aria-label={t('params.cleaning.cleaningType.label')}>
|
||||
{TYPES.map((v) => (
|
||||
<button key={v} type="button" role="radio" aria-checked={type === v} onClick={() => setType(v)}>
|
||||
{t(`params.cleaning.cleaningType.${v}`)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="v5-row">
|
||||
<span>{t('params.cleaning.rooms.label')}</span>
|
||||
<Stepper value={rooms} min={1} max={6} onChange={setRooms} decLabel={t('common.decrease')} incLabel={t('common.increase')} />
|
||||
</div>
|
||||
<div className="v5-row">
|
||||
<span>{t('landing.v5.perMonth')}</span>
|
||||
<Stepper value={count} min={1} max={8} onChange={setCount} decLabel={t('common.decrease')} incLabel={t('common.increase')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="v5-result" aria-live="polite">
|
||||
<div className="v5-save">
|
||||
<span className="v5-save__label">{save > 0 ? t('landing.v5.save') : t('landing.v5.noSave')}</span>
|
||||
<span className="v5-save__num">{save > 0 ? amd(save) : '0 ֏'}</span>
|
||||
{save > 0 && <span className="v5-save__year">{t('landing.v5.perYear', { amount: amd(save * 12) })}</span>}
|
||||
</div>
|
||||
<div className="v5-bars">
|
||||
<div className="v5-bar">
|
||||
<span className="v5-bar__label">{t('landing.v5.without')}</span>
|
||||
<span className="v5-bar__track">
|
||||
<span className="v5-bar__fill v5-bar__fill--without" style={{ width: `${(without / max) * 100}%` }} />
|
||||
</span>
|
||||
<span className="v5-bar__num">{amd(without)}</span>
|
||||
</div>
|
||||
<div className="v5-bar">
|
||||
<span className="v5-bar__label">
|
||||
{t('landing.v5.with')}
|
||||
<small>{t('landing.v5.withNote', { price: amd(monthlyPrice) })}</small>
|
||||
</span>
|
||||
<span className="v5-bar__track">
|
||||
<span className="v5-bar__fill v5-bar__fill--with" style={{ width: `${(withPlus / max) * 100}%` }} />
|
||||
</span>
|
||||
<span className="v5-bar__num">{amd(withPlus)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{breakeven !== null && (
|
||||
<p className="v5-breakeven">{breakeven === 1 ? t('landing.v5.breakevenFirst') : t('landing.v5.breakeven', { count: breakeven })}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user