feat(mera/b2c): результат проверки в облике отчёта (макет 29.08 «МЕРА отчёт»)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 59s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 59s
Редизайн результата /estimate по макету отчёта: шапка объекта (адрес + чипы параметров + «Изменить данные»), две большие бесплатные карточки, тизер платной части с shimmer-строками, честные состояния «мало данных» и «город не покрыт» в стилистике «ЧЕСТНЫЙ ОТВЕТ». Сознательные расхождения с макетом (задокументированы в шапке компонента): - «✓ расчёт готов и ждёт вас» не воспроизводится — платного расчёта для анонима не существует (анонимный /estimate закрыт, платёжного контура нет, #2896); тизер честно называется «что будет в полном отчёте», вместо кнопки оплаты — прежняя формулировка «приём оплаты подключается»; - бейджи МИР/СБП/VISA — за тем же TODO платёжного контура, что и в FooterV3; - email-подписка «сообщим когда добавим» — нет хранения и согласия (#2895), вместо неё живой канал поддержки; - «Расширить радиус до 3 км» — /coverage радиус не параметризует; - подписи бесплатных плиток — запиненные честные из coverage-copy. Возврат 150 ₽ — строкой из макета со ссылкой на /refund. Старый CoverageResult удалён (один рендер результата, не два). Фикстурное превью всех трёх тонов — /mera-public/ui-preview/report. Проверено: tsc чисто, lint чисто, vitest 27 passed, скрин трёх состояний. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
be7d7d0205
commit
de66826b25
4 changed files with 558 additions and 42 deletions
|
|
@ -37,6 +37,8 @@ import {
|
||||||
import type { AddressSuggestion } from "../../public-api";
|
import type { AddressSuggestion } from "../../public-api";
|
||||||
import styles from "../../landing-v3.module.css";
|
import styles from "../../landing-v3.module.css";
|
||||||
|
|
||||||
|
import { ReportResult } from "./ReportResult";
|
||||||
|
|
||||||
/** Задержка перед запросом подсказок. Каждый вызов платный (DaData-тир). */
|
/** Задержка перед запросом подсказок. Каждый вызов платный (DaData-тир). */
|
||||||
const SUGGEST_DEBOUNCE_MS = 300;
|
const SUGGEST_DEBOUNCE_MS = 300;
|
||||||
const SUGGEST_MIN_CHARS = 3;
|
const SUGGEST_MIN_CHARS = 3;
|
||||||
|
|
@ -442,49 +444,20 @@ export function EstimateFlow() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{phase.kind === "result" && <CoverageResult verdict={phase.verdict} />}
|
{phase.kind === "result" && (
|
||||||
|
<ReportResult
|
||||||
|
verdict={phase.verdict}
|
||||||
|
object={{
|
||||||
|
address: query,
|
||||||
|
roomsLabel:
|
||||||
|
ROOM_OPTIONS.find((o) => String(o.value) === rooms)?.label ?? null,
|
||||||
|
area: area || null,
|
||||||
|
floor: null,
|
||||||
|
}}
|
||||||
|
onEdit={() => setPhase(FORM)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CoverageResult({ verdict }: { verdict: CoverageVerdict }) {
|
|
||||||
return (
|
|
||||||
<div className={styles.estResult}>
|
|
||||||
<p className={styles.heroResultEyebrow}>
|
|
||||||
{verdict.tone === "none" ? "ПРОВЕРИЛИ" : "ГОТОВО · БЕСПЛАТНАЯ ЧАСТЬ"}
|
|
||||||
</p>
|
|
||||||
<p className={styles.heroFormFeedbackTitle}>{verdict.title}</p>
|
|
||||||
|
|
||||||
{verdict.tiles.length > 0 && (
|
|
||||||
<div className={styles.heroResultGrid}>
|
|
||||||
{verdict.tiles.map((tile) => (
|
|
||||||
<div key={tile.label} className={styles.heroResultTile}>
|
|
||||||
<div className={styles.heroResultTileValue}>{tile.value}</div>
|
|
||||||
<div className={styles.heroResultTileLabel}>{tile.label}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{verdict.lines.map((line) => (
|
|
||||||
<p key={line} className={styles.heroFormFeedbackText}>
|
|
||||||
{line}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{verdict.offerPaidStep && (
|
|
||||||
<div className={styles.estPaidStep}>
|
|
||||||
<p className={styles.heroResultOverlayTitle}>
|
|
||||||
Цена и срок продажи — следующий шаг
|
|
||||||
</p>
|
|
||||||
<p className={styles.heroFormFeedbackText}>
|
|
||||||
Полный расчёт стоит 150 ₽: диапазон цены, цена за квадратный
|
|
||||||
метр и список квартир, на которых он построен. Приём оплаты сейчас
|
|
||||||
подключается — как только заработает, кнопка появится здесь.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,214 @@
|
||||||
|
/**
|
||||||
|
* ReportResult — результат пробы покрытия в облике «отчёта» (макет 29.08,
|
||||||
|
* «МЕРА отчёт.dc.html»): шапка объекта с чипами параметров, бесплатные
|
||||||
|
* карточки, тизер платной части, честные отказы «город не покрыт» и
|
||||||
|
* «мало данных».
|
||||||
|
*
|
||||||
|
* ЧЕСТНЫЕ АДАПТАЦИИ МАКЕТА (каждая — сознательное расхождение):
|
||||||
|
*
|
||||||
|
* 1. Макетная плашка «✓ РАСЧЁТ ПО ЭТОЙ КВАРТИРЕ ГОТОВ» и «✓ рассчитана» у
|
||||||
|
* shimmer-строк НЕ воспроизводятся: платного расчёта для анонима не
|
||||||
|
* существует (анонимный /estimate закрыт, платёжного контура нет — #2896).
|
||||||
|
* Утверждать «готов и ждёт вас» было бы ложью. Вместо этого тизер честно
|
||||||
|
* называется «ЧТО БУДЕТ В ПОЛНОМ ОТЧЁТЕ», а вместо кнопки оплаты — та же
|
||||||
|
* формулировка, что жила здесь раньше: «приём оплаты подключается».
|
||||||
|
* 2. Бейджи МИР/СБП/VISA из макета не рендерятся до платёжного контура —
|
||||||
|
* ровно тот же TODO, что в FooterV3.
|
||||||
|
* 3. Email-подписки «сообщим, когда добавим город/данные» из макета нет:
|
||||||
|
* для неё нужны хранение и согласие (#2895), которых нет. Вместо неё —
|
||||||
|
* живой канал поддержки (SUPPORT_TELEGRAM_URL).
|
||||||
|
* 4. Кнопка «Расширить радиус до 3 км» не рендерится: `/coverage` радиус не
|
||||||
|
* параметризует, а обещать перепроверку, которой не будет, нельзя.
|
||||||
|
* 5. Подписи бесплатных плиток — из coverage-copy (запинены тестом): там
|
||||||
|
* «возраст активного объявления», а НЕ макетное «срок продажи по дому» —
|
||||||
|
* выборка цензурирована, честное имя величины важнее красоты.
|
||||||
|
*
|
||||||
|
* Возврат 150 ₽ при ненадёжном расчёте — политика с /refund; в тизере она
|
||||||
|
* упоминается той же строкой, что в макете, со ссылкой на полный текст.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SUPPORT_TELEGRAM_LABEL, SUPPORT_TELEGRAM_URL } from "../../content";
|
||||||
|
import type { CoverageVerdict } from "../../coverage-copy";
|
||||||
|
import styles from "../../landing-v3.module.css";
|
||||||
|
|
||||||
|
import { PublicLink } from "../PublicLink";
|
||||||
|
|
||||||
|
export interface ReportObjectParams {
|
||||||
|
readonly address: string;
|
||||||
|
readonly roomsLabel: string | null;
|
||||||
|
readonly area: string | null;
|
||||||
|
readonly floor: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
readonly verdict: CoverageVerdict;
|
||||||
|
readonly object: ReportObjectParams;
|
||||||
|
readonly onEdit: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ObjectHeader({ object, onEdit }: { object: ReportObjectParams; onEdit: () => void }) {
|
||||||
|
const chips = [
|
||||||
|
object.roomsLabel,
|
||||||
|
object.area ? `${object.area} м²` : null,
|
||||||
|
object.floor ? `Этаж ${object.floor}` : null,
|
||||||
|
].filter((c): c is string => c !== null);
|
||||||
|
return (
|
||||||
|
<header className={styles.repHead}>
|
||||||
|
<div className={styles.repHeadTop}>
|
||||||
|
<div>
|
||||||
|
<div className={styles.repEyebrow}>БЕСПЛАТНАЯ ПРОВЕРКА</div>
|
||||||
|
<h2 className={styles.repAddress}>{object.address}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{(
|
||||||
|
<div className={styles.repChipsBar}>
|
||||||
|
<div className={styles.repChips}>
|
||||||
|
{chips.map((c, i) => (
|
||||||
|
<span key={c} className={styles.repChip}>
|
||||||
|
{i > 0 && (
|
||||||
|
<span className={styles.repChipDot} aria-hidden="true">
|
||||||
|
·
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{c}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{chips.length === 0 && (
|
||||||
|
<span className={styles.repChipMuted}>параметры не указаны</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button type="button" onClick={onEdit} className={styles.repEditBtn}>
|
||||||
|
Изменить данные →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Отказ «город не покрыт» / «дом не найден» (tone none). */
|
||||||
|
function RefusalCard({ verdict }: { verdict: CoverageVerdict }) {
|
||||||
|
return (
|
||||||
|
<div className={styles.repRefusal}>
|
||||||
|
<div className={styles.repRefusalEyebrow}>ЧЕСТНЫЙ ОТВЕТ</div>
|
||||||
|
<h3 className={styles.repRefusalTitle}>{verdict.title}</h3>
|
||||||
|
{verdict.lines.map((line) => (
|
||||||
|
<p key={line} className={styles.repRefusalText}>
|
||||||
|
{line}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
<p className={styles.repRefusalText}>
|
||||||
|
Продавать отчёт «на глазок» мы не будем — поэтому и кнопки оплаты здесь
|
||||||
|
нет. Если хотите, чтобы мы сообщили, когда данные появятся, — напишите в
|
||||||
|
поддержку:{" "}
|
||||||
|
<a href={SUPPORT_TELEGRAM_URL} target="_blank" rel="noreferrer">
|
||||||
|
{SUPPORT_TELEGRAM_LABEL}
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Бесплатные карточки (tone ok/thin) — числа и подписи из verdict.tiles. */
|
||||||
|
function FreeCards({ verdict }: { verdict: CoverageVerdict }) {
|
||||||
|
return (
|
||||||
|
<div className={styles.repFreeGrid}>
|
||||||
|
{verdict.tiles.map((tile) => (
|
||||||
|
<div key={tile.label} className={styles.repFreeCard}>
|
||||||
|
<div className={styles.repFreeEyebrow}>✓ БЕСПЛАТНО</div>
|
||||||
|
<div className={styles.repFreeRow}>
|
||||||
|
<span className={styles.repFreeValue}>{tile.value}</span>
|
||||||
|
<span className={styles.repFreeLabel}>{tile.label}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAID_ROWS = [
|
||||||
|
{ label: "РЕКОМЕНДОВАННАЯ ЦЕНА", w: "60%" },
|
||||||
|
{ label: "ПРОГНОЗ СРОКА ПРОДАЖИ ПРИ ЭТОЙ ЦЕНЕ", w: "45%" },
|
||||||
|
{ label: "ДИАПАЗОН И ПОЛОЖЕНИЕ НА РЫНКЕ", w: "70%" },
|
||||||
|
{ label: "АНАЛОГИ С АДРЕСАМИ И ЦЕНАМИ", w: "88%" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** Тизер платной части (tone ok): shimmer-строки + честная правая панель. */
|
||||||
|
function PaidTeaser() {
|
||||||
|
return (
|
||||||
|
<div className={styles.repPaid}>
|
||||||
|
<div className={styles.repPaidBar}>
|
||||||
|
<span className={styles.repPaidBarText}>ПОЛНЫЙ ОТЧЁТ · 150 ₽</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.repPaidGrid}>
|
||||||
|
<div className={styles.repPaidLeft}>
|
||||||
|
{PAID_ROWS.map((row) => (
|
||||||
|
<div key={row.label}>
|
||||||
|
<div className={styles.repPaidRowLabel}>{row.label}</div>
|
||||||
|
<div className={styles.repPaidRowLine}>
|
||||||
|
<span
|
||||||
|
className={styles.repShimmer}
|
||||||
|
style={{ width: row.w }}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span className={styles.repPaidRowChip}>в отчёте</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className={styles.repPaidRight}>
|
||||||
|
<div className={styles.repPaidTitle}>
|
||||||
|
Цена и срок продажи — следующий шаг
|
||||||
|
</div>
|
||||||
|
<p className={styles.repPaidText}>
|
||||||
|
Полный расчёт стоит 150 ₽: диапазон цены, цена за квадратный
|
||||||
|
метр и список квартир, на которых он построен. Один платёж, без
|
||||||
|
подписки. Приём оплаты сейчас подключается — как только заработает,
|
||||||
|
кнопка появится здесь.
|
||||||
|
</p>
|
||||||
|
<p className={styles.repPaidFine}>
|
||||||
|
Если надёжный расчёт не получится — вернём 150 ₽:{" "}
|
||||||
|
<PublicLink to="refund">условия возврата</PublicLink>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReportResult({ verdict, object, onEdit }: Props) {
|
||||||
|
return (
|
||||||
|
<div className={styles.repRoot}>
|
||||||
|
<ObjectHeader object={object} onEdit={onEdit} />
|
||||||
|
{verdict.tone === "none" ? (
|
||||||
|
<RefusalCard verdict={verdict} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{verdict.tiles.length > 0 && <FreeCards verdict={verdict} />}
|
||||||
|
{verdict.lines.length > 0 && (
|
||||||
|
<div className={styles.repNotes}>
|
||||||
|
{verdict.lines.map((line) => (
|
||||||
|
<p key={line}>{line}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{verdict.tone === "thin" && (
|
||||||
|
<div className={styles.repRefusal}>
|
||||||
|
<div className={styles.repRefusalEyebrow}>ЧЕСТНЫЙ ОТВЕТ</div>
|
||||||
|
<h3 className={styles.repRefusalTitle}>
|
||||||
|
Для платного расчёта данных пока мало
|
||||||
|
</h3>
|
||||||
|
<p className={styles.repRefusalText}>
|
||||||
|
Прогноз на паре точек был бы гаданием, поэтому кнопки оплаты
|
||||||
|
здесь нет. Когда объявлений рядом станет больше, полный отчёт
|
||||||
|
откроется по этому же адресу.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{verdict.offerPaidStep && <PaidTeaser />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2164,3 +2164,249 @@
|
||||||
font-size: 17.5px;
|
font-size: 17.5px;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Отчёт (макет 29.08): шапка объекта, бесплатные карточки, тизер ------- */
|
||||||
|
.repRoot {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.repHead {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.repHeadTop {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.repEyebrow {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-family: var(--b2c-font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
color: var(--b2c-accent-text);
|
||||||
|
}
|
||||||
|
.repAddress {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(24px, 3.4vw, 34px);
|
||||||
|
line-height: 1.1;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
font-weight: 800;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.repChipsBar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px 28px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 16px 22px;
|
||||||
|
background: var(--b2c-surface);
|
||||||
|
border: 1px solid var(--b2c-line);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.repChips {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 14.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.repChip {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
.repChipDot {
|
||||||
|
color: var(--b2c-line);
|
||||||
|
}
|
||||||
|
.repChipMuted {
|
||||||
|
color: var(--b2c-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.repEditBtn {
|
||||||
|
margin-left: auto;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: var(--b2c-accent-text);
|
||||||
|
font-family: var(--b2c-font-sans);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 13.5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.repEditBtn:hover {
|
||||||
|
color: var(--b2c-ink);
|
||||||
|
}
|
||||||
|
.repRefusal {
|
||||||
|
max-width: 720px;
|
||||||
|
padding: clamp(24px, 4vw, 40px);
|
||||||
|
background: var(--b2c-surface);
|
||||||
|
border: 1px solid var(--b2c-line);
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
.repRefusalEyebrow {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
font-family: var(--b2c-font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
color: var(--b2c-muted);
|
||||||
|
}
|
||||||
|
.repRefusalTitle {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: clamp(20px, 2.6vw, 27px);
|
||||||
|
line-height: 1.2;
|
||||||
|
letter-spacing: -0.015em;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
.repRefusalText {
|
||||||
|
margin: 0 0 14px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.65;
|
||||||
|
color: var(--b2c-muted);
|
||||||
|
text-wrap: pretty;
|
||||||
|
}
|
||||||
|
.repRefusalText:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.repFreeGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.repFreeCard {
|
||||||
|
padding: clamp(22px, 3vw, 30px);
|
||||||
|
background: var(--b2c-surface);
|
||||||
|
border: 1px solid var(--b2c-line);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.repFreeEyebrow {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-family: var(--b2c-font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
color: var(--b2c-accent-text);
|
||||||
|
}
|
||||||
|
.repFreeRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.repFreeValue {
|
||||||
|
font-size: clamp(32px, 4.4vw, 46px);
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
line-height: 1;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.repFreeLabel {
|
||||||
|
font-size: 14.5px;
|
||||||
|
color: var(--b2c-muted);
|
||||||
|
max-width: 30ch;
|
||||||
|
}
|
||||||
|
.repNotes p {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--b2c-muted);
|
||||||
|
}
|
||||||
|
.repNotes p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.repPaid {
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--b2c-surface);
|
||||||
|
border: 1px solid var(--b2c-line);
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
.repPaidBar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: var(--b2c-surface-soft);
|
||||||
|
}
|
||||||
|
.repPaidBarText {
|
||||||
|
font-family: var(--b2c-font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
color: var(--b2c-accent-text);
|
||||||
|
}
|
||||||
|
.repPaidGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
|
||||||
|
}
|
||||||
|
.repPaidLeft {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18px;
|
||||||
|
padding: clamp(22px, 3.5vw, 34px);
|
||||||
|
}
|
||||||
|
.repPaidRowLabel {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-family: var(--b2c-font-mono);
|
||||||
|
font-size: 9.5px;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
color: var(--b2c-muted);
|
||||||
|
}
|
||||||
|
.repPaidRowLine {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.repShimmer {
|
||||||
|
display: inline-block;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: linear-gradient(90deg, #eaf0f1 25%, #f4f7f8 50%, #eaf0f1 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: repShimmer 1.8s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes repShimmer {
|
||||||
|
from { background-position: 200% 0; }
|
||||||
|
to { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.repShimmer { animation: none; }
|
||||||
|
}
|
||||||
|
.repPaidRowChip {
|
||||||
|
font-family: var(--b2c-font-mono);
|
||||||
|
font-size: 10.5px;
|
||||||
|
color: var(--b2c-accent-text);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 1px solid var(--b2c-line);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
}
|
||||||
|
.repPaidRight {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: clamp(22px, 3.5vw, 34px);
|
||||||
|
border-left: 1px solid var(--b2c-line-soft);
|
||||||
|
background: var(--b2c-page-bg);
|
||||||
|
}
|
||||||
|
.repPaidTitle {
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: clamp(18px, 2.1vw, 22px);
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.repPaidText {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14.5px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--b2c-muted);
|
||||||
|
}
|
||||||
|
.repPaidFine {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: #9fb0b4;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Фикстурное превью ReportResult — все три тона вердикта на одной странице.
|
||||||
|
*
|
||||||
|
* Зачем: результат пробы покрытия зависит от живого бэкенда за публичным
|
||||||
|
* периметром Caddy — локально его не воспроизвести, не подделывая секрет
|
||||||
|
* периметра. Превью рендерит компонент на фикстурах (та же конвенция, что
|
||||||
|
* `app/ui-preview/estimate` у B2B-части: NEXT_PUBLIC_ENABLE_PREVIEW=1, иначе
|
||||||
|
* notFound; в прод-сборке страница недостижима и в Caddy-allowlist публичного
|
||||||
|
* домена не входит).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
|
import { ReportResult } from "../../_components/estimate/ReportResult";
|
||||||
|
import type { CoverageVerdict } from "../../coverage-copy";
|
||||||
|
|
||||||
|
const ENABLED = process.env.NEXT_PUBLIC_ENABLE_PREVIEW === "1";
|
||||||
|
|
||||||
|
const OK: CoverageVerdict = {
|
||||||
|
tone: "ok",
|
||||||
|
title: "Данных достаточно",
|
||||||
|
lines: [
|
||||||
|
"Проверили по вашему дому и окружению в радиусе 1 км.",
|
||||||
|
],
|
||||||
|
tiles: [
|
||||||
|
{ value: "14", label: "похожих квартир продаётся в радиусе 1 км" },
|
||||||
|
{ value: "47 дн.", label: "медианный возраст активного объявления" },
|
||||||
|
],
|
||||||
|
offerPaidStep: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const THIN: CoverageVerdict = {
|
||||||
|
tone: "thin",
|
||||||
|
title: "Похожие квартиры рядом есть, но их мало",
|
||||||
|
lines: ["Нашли 2 похожие квартиры в радиусе 1 км."],
|
||||||
|
tiles: [{ value: "2", label: "похожие квартиры продаются в радиусе 1 км" }],
|
||||||
|
offerPaidStep: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const NONE: CoverageVerdict = {
|
||||||
|
tone: "none",
|
||||||
|
title: "Ваш город мы пока не оцениваем",
|
||||||
|
lines: [
|
||||||
|
"Чтобы прогноз был честным, нужен объём данных по сделкам и объявлениям, которого в этом городе у нас пока нет.",
|
||||||
|
],
|
||||||
|
tiles: [],
|
||||||
|
offerPaidStep: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const OBJECT = {
|
||||||
|
address: "Екатеринбург, ул. Инженерная, 14",
|
||||||
|
roomsLabel: "2 комнаты",
|
||||||
|
area: "54,2",
|
||||||
|
floor: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ReportPreviewPage() {
|
||||||
|
if (!ENABLED) notFound();
|
||||||
|
return (
|
||||||
|
<main
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 48,
|
||||||
|
maxWidth: 960,
|
||||||
|
margin: "0 auto",
|
||||||
|
padding: "32px 20px 96px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<ReportResult verdict={OK} object={OBJECT} onEdit={() => {}} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ReportResult verdict={THIN} object={OBJECT} onEdit={() => {}} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ReportResult verdict={NONE} object={OBJECT} onEdit={() => {}} />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue