-
- {verdict.tone === "none" ? "ПРОВЕРИЛИ" : "ГОТОВО · БЕСПЛАТНАЯ ЧАСТЬ"}
-
-
{verdict.title}
-
- {verdict.tiles.length > 0 && (
-
- {verdict.tiles.map((tile) => (
-
-
{tile.value}
-
{tile.label}
-
- ))}
-
- )}
-
- {verdict.lines.map((line) => (
-
- {line}
-
- ))}
-
- {verdict.offerPaidStep && (
-
-
- Цена и срок продажи — следующий шаг
-
-
- Полный расчёт стоит 150 ₽: диапазон цены, цена за квадратный
- метр и список квартир, на которых он построен. Приём оплаты сейчас
- подключается — как только заработает, кнопка появится здесь.
-
-
- )}
-
- );
-}
diff --git a/tradein-mvp/frontend/src/app/mera-public/_components/estimate/ReportResult.tsx b/tradein-mvp/frontend/src/app/mera-public/_components/estimate/ReportResult.tsx
new file mode 100644
index 00000000..13dbf946
--- /dev/null
+++ b/tradein-mvp/frontend/src/app/mera-public/_components/estimate/ReportResult.tsx
@@ -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 (
+
+
ЧЕСТНЫЙ ОТВЕТ
+
{verdict.title}
+ {verdict.lines.map((line) => (
+
+ {line}
+
+ ))}
+
+ Продавать отчёт «на глазок» мы не будем — поэтому и кнопки оплаты здесь
+ нет. Если хотите, чтобы мы сообщили, когда данные появятся, — напишите в
+ поддержку:{" "}
+
+ {SUPPORT_TELEGRAM_LABEL}
+
+ .
+
+
+ );
+}
+
+/** Бесплатные карточки (tone ok/thin) — числа и подписи из verdict.tiles. */
+function FreeCards({ verdict }: { verdict: CoverageVerdict }) {
+ return (
+
+
+ {verdict.tone === "none" ? (
+
+ ) : (
+ <>
+ {verdict.tiles.length > 0 &&
}
+ {verdict.lines.length > 0 && (
+
+ {verdict.lines.map((line) => (
+
{line}
+ ))}
+
+ )}
+ {verdict.tone === "thin" && (
+
+
ЧЕСТНЫЙ ОТВЕТ
+
+ Для платного расчёта данных пока мало
+
+
+ Прогноз на паре точек был бы гаданием, поэтому кнопки оплаты
+ здесь нет. Когда объявлений рядом станет больше, полный отчёт
+ откроется по этому же адресу.
+
+
+ )}
+ {verdict.offerPaidStep &&
}
+ >
+ )}
+
+ );
+}
diff --git a/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css b/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css
index eb9cb4ca..367eb195 100644
--- a/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css
+++ b/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css
@@ -2164,3 +2164,249 @@
font-size: 17.5px;
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;
+}
diff --git a/tradein-mvp/frontend/src/app/mera-public/ui-preview/report/page.tsx b/tradein-mvp/frontend/src/app/mera-public/ui-preview/report/page.tsx
new file mode 100644
index 00000000..a01170a4
--- /dev/null
+++ b/tradein-mvp/frontend/src/app/mera-public/ui-preview/report/page.tsx
@@ -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 (
+