fix(mera/b2c): снять с витрины обещание времени и подписать величины измеренным

«30 секунд» ушло из бейджа шага 1 как незамеренное, но осталось видимым
посетителю в четырёх других местах той же страницы: подпись под кнопкой формы,
CTA шапки, CTA блока возражений и липкая нижняя панель, которая висит на экране
всегда. Гейт витринных чисел этого не видел по построению — «сек» не было среди
единиц, а трёх файлов не было в списке сканируемых (обе дыры закрыты отдельно).
Замена — по смыслу: число полей формы (STEP1_FIELDS_LABEL) проверяется глазами,
и оба места берут его из landing-facts.ts, а не пишут у себя.

Медиана аналогов стояла под общей шапкой «пример по рынку Екатеринбурга», но
городом не ограничена: _ANALOGS_SQL идёт по всей таблице trade_in_estimates,
фильтр по city стоит только у экспозиции. Плюс Hero печатал у неё лишь размер
выборки и выбрасывал note «только расчёты, где аналоги нашлись» — отбор
выживших. Теперь у каждой величины своя подпись со своей границей и своя
оговорка рядом со значением; правило зафиксировано двумя проверками рендера
(обе краснеют на возврате к одной строке и на выброшенном note).

price_cut_share_pct считает долю ОБЪЯВЛЕНИЙ и только по Домклику — подпись
«столько продавцов снижают цену» приписывала величине единицу, которой её не
мерили. Один продавец ведёт несколько объявлений.

В ReportResult оставались три литерала «150 ₽» при живой SERVICE_PRICE_RUB:
цена обязана иметь один источник с офертой и политикой возврата.

Обещание «прогноз срока продажи» приведено к тому, что считается:
_estimate_days_on_market — медиана days_on_market аналогов, то есть сколько
похожие объявления УЖЕ висят. deals.days_on_market заполнен 0 раз из 108 623,
сверять прогноз срока не с чем (по этой же причине в ветке уже снято «±6 дн»).
This commit is contained in:
bot-backend 2026-08-29 20:28:10 +05:00
parent 849a4b2c3d
commit eb494e76fb
16 changed files with 163 additions and 47 deletions

View file

@ -11,6 +11,7 @@ import { describe, expect, it } from "vitest";
import { AccuracyV3 } from "../_components/v3/AccuracyV3"; import { AccuracyV3 } from "../_components/v3/AccuracyV3";
import { CostOfErrorV3 } from "../_components/v3/CostOfErrorV3"; import { CostOfErrorV3 } from "../_components/v3/CostOfErrorV3";
import { HeroV3 } from "../_components/v3/HeroV3";
import type { LandingStat, ShowcaseResponse } from "../public-api"; import type { LandingStat, ShowcaseResponse } from "../public-api";
const stat = (value: number, sample_n: number | null, note: string): LandingStat => ({ const stat = (value: number, sample_n: number | null, note: string): LandingStat => ({
@ -28,6 +29,16 @@ const STATS = {
price_cut_median_pct_per_month: stat(-2.17, 3018, "Медианное изменение цены за 30 дней среди снижавших"), price_cut_median_pct_per_month: stat(-2.17, 3018, "Медианное изменение цены за 30 дней среди снижавших"),
}; };
/** Hero печатает ещё и медиану аналогов — величину БЕЗ фильтра по городу. */
const HERO_STATS = {
...STATS,
analogs_median: stat(
14,
1123,
"Медиана числа аналогов на расчёт (только расчёты, где аналоги нашлись)",
),
};
const SHOWCASE: ShowcaseResponse = { const SHOWCASE: ShowcaseResponse = {
computed_at: "2026-08-29T03:00:00+00:00", computed_at: "2026-08-29T03:00:00+00:00",
deals: [ deals: [
@ -88,7 +99,35 @@ describe("витрина лэндинга v3 без данных", () => {
it("«Цена ошибки»: без /stats остаётся только цена — она из оферты, а не из замера", () => { it("«Цена ошибки»: без /stats остаётся только цена — она из оферты, а не из замера", () => {
render(<CostOfErrorV3 stats={{}} />); render(<CostOfErrorV3 stats={{}} />);
expect(screen.queryByText(/столько продавцов снижают цену/)).toBeNull(); expect(screen.queryByText(/столько объявлений на Домклике снижали цену/)).toBeNull();
expect(screen.getByText("150 ₽")).toBeTruthy(); expect(screen.getByText("150 ₽")).toBeTruthy();
}); });
/**
* Величина не должна стоять под подписью, к которой не привязана. Экспозиция
* считается с фильтром по городу (`_LISTING_AGE_SQL`), медиана аналогов по
* всей таблице `trade_in_estimates`; под общей шапкой «пример по рынку
* Екатеринбурга» вторая была бы подписана чужой границей. Проверяем не
* наличие двух строк, а СОДЕРЖИМОЕ каждой: слить их обратно в одну и
* оставить тест зелёным невозможно.
*/
it("HeroV3: аналоги подписаны своей границей и своей оговоркой, а не Екатеринбургом", () => {
render(<HeroV3 stats={HERO_STATS} />);
const ekbLine = screen.getByText(/Пример по рынку Екатеринбурга/).parentElement;
expect(ekbLine?.textContent).toMatch(/активное объявление висит\s+26\s+дн\./u);
expect(ekbLine?.textContent, "экспозиция и аналоги снова в одной строке").not.toMatch(/аналог/);
const allLine = screen.getByText(/По всем расчётам МЕРЫ/).parentElement;
expect(allLine?.textContent).toMatch(/медиана аналогов на расчёт/);
// Отбор выживших: расчёты без аналогов в знаменатель не попали.
expect(allLine?.textContent, "note бэкенда выброшен").toMatch(
/только расчёты, где аналоги нашлись/,
);
});
it("HeroV3: без /stats обе строки примера исчезают, а не показывают подпись без числа", () => {
render(<HeroV3 stats={{}} />);
expect(screen.queryByText(/Пример по рынку Екатеринбурга/)).toBeNull();
expect(screen.queryByText(/По всем расчётам МЕРЫ/)).toBeNull();
});
}); });

View file

@ -8,7 +8,7 @@ import { PublicLink } from "./PublicLink";
* (`v3/HeaderV3.tsx`). * (`v3/HeaderV3.tsx`).
* *
* Из неё убраны навигация по секциям лэндинга, селектор города и CTA * Из неё убраны навигация по секциям лэндинга, селектор города и CTA
* «Проверить за 30 секунд». Причина у всех трёх одна: человек уже ушёл с * «Проверить квартиру». Причина у всех трёх одна: человек уже ушёл с
* лэндинга. Якоря вроде `#accuracy` тут просто не на что навести (секций нет * лэндинга. Якоря вроде `#accuracy` тут просто не на что навести (секций нет
* на этих страницах), город выбирается прямо в форме проверки а второй * на этих страницах), город выбирается прямо в форме проверки а второй
* селектор города в шапке означал бы два места, задающих одно значение. * селектор города в шапке означал бы два места, задающих одно значение.

View file

@ -23,11 +23,24 @@
* «возраст активного объявления», а НЕ макетное «срок продажи по дому» * «возраст активного объявления», а НЕ макетное «срок продажи по дому»
* выборка цензурирована, честное имя величины важнее красоты. * выборка цензурирована, честное имя величины важнее красоты.
* *
* Возврат 150 при ненадёжном расчёте политика с /refund; в тизере она * Возврат при ненадёжном расчёте политика с /refund; в тизере она
* упоминается той же строкой, что в макете, со ссылкой на полный текст. * упоминается той же строкой, что в макете, со ссылкой на полный текст.
*
* ЦЕНА ТОЛЬКО `SERVICE_PRICE_RUB`. Здесь оставались три литерала «150 »
* (плашка тизера, текст и строка возврата). Цена стоит в оферте и в политике
* возврата; вписанная в экран копия разъехалась бы с ними молча, и человек
* увидел бы одну цифру на витрине, а другую в документе, который его
* обязывает.
*
* СРОК. Строка тизера обещала «прогноз срока продажи»: платный расчёт отдаёт
* `est_days_on_market` медиану `days_on_market` аналогов, то есть сколько
* похожие объявления УЖЕ висят. `deals.days_on_market` заполнен 0 раз из
* 108 623, поэтому сверить прогноз срока не с чем и прогнозом эта величина
* не является. Названа тем, что считается, ровно как в пункте 5 выше про
* бесплатные плитки.
*/ */
import { SUPPORT_TELEGRAM_LABEL, SUPPORT_TELEGRAM_URL } from "../../content"; import { SERVICE_PRICE_RUB, SUPPORT_TELEGRAM_LABEL, SUPPORT_TELEGRAM_URL } from "../../content";
import type { CoverageVerdict } from "../../coverage-copy"; import type { CoverageVerdict } from "../../coverage-copy";
import styles from "../../landing-v3.module.css"; import styles from "../../landing-v3.module.css";
@ -129,7 +142,7 @@ function FreeCards({ verdict }: { verdict: CoverageVerdict }) {
const PAID_ROWS = [ const PAID_ROWS = [
{ label: "РЕКОМЕНДОВАННАЯ ЦЕНА", w: "60%" }, { label: "РЕКОМЕНДОВАННАЯ ЦЕНА", w: "60%" },
{ label: "ПРОГНОЗ СРОКА ПРОДАЖИ ПРИ ЭТОЙ ЦЕНЕ", w: "45%" }, { label: "СКОЛЬКО УЖЕ ВИСЯТ ПОХОЖИЕ ОБЪЯВЛЕНИЯ", w: "45%" },
{ label: "ДИАПАЗОН И ПОЛОЖЕНИЕ НА РЫНКЕ", w: "70%" }, { label: "ДИАПАЗОН И ПОЛОЖЕНИЕ НА РЫНКЕ", w: "70%" },
{ label: "АНАЛОГИ С АДРЕСАМИ И ЦЕНАМИ", w: "88%" }, { label: "АНАЛОГИ С АДРЕСАМИ И ЦЕНАМИ", w: "88%" },
] as const; ] as const;
@ -139,7 +152,9 @@ function PaidTeaser() {
return ( return (
<div className={styles.repPaid}> <div className={styles.repPaid}>
<div className={styles.repPaidBar}> <div className={styles.repPaidBar}>
<span className={styles.repPaidBarText}>ПОЛНЫЙ ОТЧЁТ · 150 </span> <span className={styles.repPaidBarText}>
ПОЛНЫЙ ОТЧЁТ · {SERVICE_PRICE_RUB}&nbsp;
</span>
</div> </div>
<div className={styles.repPaidGrid}> <div className={styles.repPaidGrid}>
<div className={styles.repPaidLeft}> <div className={styles.repPaidLeft}>
@ -159,17 +174,17 @@ function PaidTeaser() {
</div> </div>
<div className={styles.repPaidRight}> <div className={styles.repPaidRight}>
<div className={styles.repPaidTitle}> <div className={styles.repPaidTitle}>
Цена и срок продажи следующий шаг Ваша цена следующий шаг
</div> </div>
<p className={styles.repPaidText}> <p className={styles.repPaidText}>
Полный расчёт стоит 150&nbsp;: диапазон цены, цена за квадратный Полный расчёт стоит {SERVICE_PRICE_RUB}&nbsp;: диапазон цены, цена
метр и список квартир, на которых он построен. Один платёж, без за квадратный метр и список квартир, на которых он построен. Один
подписки. Приём оплаты сейчас подключается как только заработает, платёж, без подписки. Приём оплаты сейчас подключается как только
кнопка появится здесь. заработает, кнопка появится здесь.
</p> </p>
<p className={styles.repPaidFine}> <p className={styles.repPaidFine}>
Если надёжный расчёт не получится вернём 150&nbsp;:{" "} Если надёжный расчёт не получится вернём {SERVICE_PRICE_RUB}
<PublicLink to="refund">условия возврата</PublicLink>. &nbsp;: <PublicLink to="refund">условия возврата</PublicLink>.
</p> </p>
</div> </div>
</div> </div>

View file

@ -9,6 +9,13 @@
* снижал»: одна подпись на две величины была бы подменой знаменателя. * снижал»: одна подпись на две величины была бы подменой знаменателя.
* Цена `SERVICE_PRICE_RUB` (content.ts), тот же источник, что у оферты. * Цена `SERVICE_PRICE_RUB` (content.ts), тот же источник, что у оферты.
* *
* ОБЪЯВЛЕНИЯ, А НЕ ПРОДАВЦЫ. `price_cut_share_pct` доля ОБЪЯВЛЕНИЙ, и
* только по Домклику (единственный источник, где триггер пишет стартовую
* цену, см. шапку `landing_stats.py`). Один продавец ведёт несколько
* объявлений, поэтому «столько продавцов снижают цену» приписывало величине
* единицу, которой её не мерили: подпись приведена к измеренному, а источник
* назван прямо в ней, а не только в `note` под плиткой.
*
* ЧЕГО ЗДЕСЬ БОЛЬШЕ НЕТ. Плитки «×2,4 дольше продаётся квартира с завышенной * ЧЕГО ЗДЕСЬ БОЛЬШЕ НЕТ. Плитки «×2,4 дольше продаётся квартира с завышенной
* ценой»: замер даёт ×1,04 (79 дней против 76), и это уже с цензурой в пользу * ценой»: замер даёт ×1,04 (79 дней против 76), и это уже с цензурой в пользу
* заявления. Её место заняла доля снижающих цену измеренная величина про то * заявления. Её место заняла доля снижающих цену измеренная величина про то
@ -51,7 +58,7 @@ export function CostOfErrorV3({ stats }: { stats: LandingStats }) {
tiles.push({ tiles.push({
key: "share", key: "share",
value: share.text, value: share.text,
title: "столько продавцов снижают цену", title: "столько объявлений на Домклике снижали цену",
text: [share.note, share.sample].filter(Boolean).join(" · "), text: [share.note, share.sample].filter(Boolean).join(" · "),
}); });
} }

View file

@ -20,6 +20,11 @@
* *
* Ветка результата ПОСТРОЕНА (`FreeResultV3.tsx`), но не подключена см. * Ветка результата ПОСТРОЕНА (`FreeResultV3.tsx`), но не подключена см.
* TODO у `PUBLIC_ESTIMATE_ENABLED` ниже и JSDoc в `FreeResultV3.tsx`. * TODO у `PUBLIC_ESTIMATE_ENABLED` ниже и JSDoc в `FreeResultV3.tsx`.
*
* ПОДПИСЬ ПОД КНОПКОЙ. В ней стояло «30 СЕКУНД» замера времени заполнения
* формы нет и не было. Его место занял `STEP1_FIELDS_LABEL`: число полей
* проверяемый факт, и оно приезжает из `landing-facts.ts`, а не вписано сюда
* второй раз (иначе с бейджем шага 1 они разъехались бы молча).
*/ */
import { useId, useRef, useState } from "react"; import { useId, useRef, useState } from "react";
@ -27,6 +32,7 @@ import type { FormEvent } from "react";
import { PRIMARY_CITY, PUBLIC_ROUTES } from "../../content"; import { PRIMARY_CITY, PUBLIC_ROUTES } from "../../content";
import { saveDraft } from "../../estimate-draft"; import { saveDraft } from "../../estimate-draft";
import { STEP1_FIELDS_LABEL } from "../../landing-facts";
import styles from "../../landing-v3.module.css"; import styles from "../../landing-v3.module.css";
/** Три состояния квартиры — дословно из макета (строка 734 исходника). */ /** Три состояния квартиры — дословно из макета (строка 734 исходника). */
@ -260,7 +266,7 @@ export function FreeCheckCard() {
Показать данные по моему дому Показать данные по моему дому
</button> </button>
<p className={styles.heroSubmitCaption}> <p className={styles.heroSubmitCaption}>
БЕЗ РЕГИСТРАЦИИ · БЕЗ ЗВОНКОВ · 30 СЕКУНД {`БЕЗ РЕГИСТРАЦИИ · БЕЗ ЗВОНКОВ · ${STEP1_FIELDS_LABEL.toUpperCase()}`}
</p> </p>
{/* Живая область держим в DOM постоянно регион, добавленный в {/* Живая область держим в DOM постоянно регион, добавленный в

View file

@ -32,9 +32,14 @@ export interface FreeResultV3Props {
analogs: string; analogs: string;
/** Подпись под числом объявлений. */ /** Подпись под числом объявлений. */
analogsLabel: string; analogsLabel: string;
/** Средний срок продажи — крупная цифра второй плитки, напр. "47 дн.". */ /**
* Экспозиция похожих объявлений крупная цифра второй плитки, напр.
* "47 дн.". НЕ срок продажи: считается по тем, кто ещё висит, и снятие
* объявления не означает сделку (см. `median_listing_age_days` в
* public-api.ts).
*/
days: string; days: string;
/** Подпись под сроком продажи. */ /** Подпись под экспозицией. */
daysLabel: string; daysLabel: string;
/** Клик по «Проверить другую квартиру» — вернуть карточку к форме. */ /** Клик по «Проверить другую квартиру» — вернуть карточку к форме. */
onBack: () => void; onBack: () => void;
@ -72,7 +77,7 @@ export function FreeResultV3({
</div> </div>
<div className={styles.heroResultOverlay}> <div className={styles.heroResultOverlay}>
<p className={styles.heroResultOverlayTitle}> <p className={styles.heroResultOverlayTitle}>
Ваша цена и срок продажи готовы Ваша цена готова
</p> </p>
<button <button
type="button" type="button"

View file

@ -70,7 +70,7 @@ export function HeaderV3() {
Для бизнеса Для бизнеса
</a> </a>
<a href="#check" className={styles.hdrCta}> <a href="#check" className={styles.hdrCta}>
Проверить за 30 секунд Проверить квартиру
</a> </a>
</div> </div>
</div> </div>

View file

@ -14,6 +14,20 @@
* конкретного человека. Цена `SERVICE_PRICE_RUB` (content.ts), один * конкретного человека. Цена `SERVICE_PRICE_RUB` (content.ts), один
* источник с офертой и политикой возврата. * источник с офертой и политикой возврата.
* *
* ДВЕ СТРОКИ ПРИМЕРА, А НЕ ОДНА. Екатеринбургом ограничена только экспозиция
* (`_LISTING_AGE_SQL` фильтрует `listings.city`); медиана аналогов считается
* по всей таблице `trade_in_estimates` без фильтра по городу. Стоя под общей
* шапкой «пример по рынку Екатеринбурга», она была бы подписана городом, к
* которому не привязана, поэтому у каждой величины своя подпись со своей
* границей. Рядом с аналогами печатается их `note` бэкенда («только расчёты,
* где аналоги нашлись»): это отбор выживших, расчёты без аналогов в
* знаменатель не попали, и без оговорки медиана читается выше, чем есть.
*
* СРОК. В тексте нет «узнаете срок продажи»: платный отчёт отдаёт
* `est_days_on_market` медиану `days_on_market` аналогов, то есть сколько
* похожие объявления УЖЕ висят (`deals.days_on_market` пуст, сверять прогноз
* срока не с чем). Обещание приведено к этой величине.
*
* ЧЕГО В ТЕКСТЕ БОЛЬШЕ НЕТ: обещания «каждый такой прогноз мы потом сверяем с * ЧЕГО В ТЕКСТЕ БОЛЬШЕ НЕТ: обещания «каждый такой прогноз мы потом сверяем с
* фактом сделки». Контура сверки прогноза клиента с его сделкой в продукте не * фактом сделки». Контура сверки прогноза клиента с его сделкой в продукте не
* существует (`trade_in_leads` 4 строки без полей исхода), и обещать его * существует (`trade_in_leads` 4 строки без полей исхода), и обещать его
@ -35,12 +49,22 @@ export function HeroV3({ stats }: { stats: LandingStats }) {
digits: 0, digits: 0,
sampleWord: "объявлениям", sampleWord: "объявлениям",
}); });
const marketExample = [ const marketLines = [
analogs && `медиана аналогов на расчёт — ${analogs.text}${analogs.sample ? ` (${analogs.sample})` : ""}`, age && {
age && `активное объявление висит ${age.text}${age.sample ? ` (медиана ${age.sample})` : ""}`, key: "age",
] label: "Пример по рынку Екатеринбурга, а не расчёт по вашему адресу:",
.filter((line): line is string => Boolean(line)) text: [`активное объявление висит ${age.text}`, age.sample && `медиана ${age.sample}`]
.join("; "); .filter(Boolean)
.join(" · "),
},
analogs && {
key: "analogs",
label: "По всем расчётам МЕРЫ, а не только по Екатеринбургу:",
text: [`медиана аналогов на расчёт — ${analogs.text}`, analogs.sample, analogs.note]
.filter(Boolean)
.join(" · "),
},
].filter((line): line is { key: string; label: string; text: string } => Boolean(line));
return ( return (
<section <section
@ -81,8 +105,8 @@ export function HeroV3({ stats }: { stats: LandingStats }) {
</span> </span>
<span className={styles.heroListText}> <span className={styles.heroListText}>
<strong>Не зависнуть:</strong> узнаете, за сколько дней уйдёт <strong>Не зависнуть:</strong> увидите, сколько времени уже
квартира по вашей цене висят похожие квартиры рядом
</span> </span>
</div> </div>
<div className={styles.heroListItem}> <div className={styles.heroListItem}>
@ -115,20 +139,20 @@ export function HeroV3({ stats }: { stats: LandingStats }) {
За{" "} За{" "}
<span className={styles.heroNoBreak}>{`${SERVICE_PRICE_RUB}`}</span>: <span className={styles.heroNoBreak}>{`${SERVICE_PRICE_RUB}`}</span>:
</strong>{" "} </strong>{" "}
ваша цена, срок продажи и аналоги, на которых построен расчёт ваша цена, экспозиция похожих объявлений и аналоги, на которых
построен расчёт
</span> </span>
</div> </div>
{marketExample !== "" && ( {marketLines.map((line) => (
<div className={styles.heroListItem}> <div key={line.key} className={styles.heroListItem}>
<span className={styles.heroListIcon} aria-hidden="true"> <span className={styles.heroListIcon} aria-hidden="true">
</span> </span>
<span className={styles.heroListText}> <span className={styles.heroListText}>
<strong>Пример по рынку Екатеринбурга, а не расчёт по вашему адресу:</strong>{" "} <strong>{line.label}</strong> {line.text}
{marketExample}
</span> </span>
</div> </div>
)} ))}
</div> </div>
</div> </div>

View file

@ -73,7 +73,7 @@ export function ObjectionsV3() {
Что обычно спрашивают перед оплатой Что обычно спрашивают перед оплатой
</h2> </h2>
<PublicLink to="estimate" className={styles.objCtaBtn}> <PublicLink to="estimate" className={styles.objCtaBtn}>
Проверить за 30 секунд Проверить квартиру
</PublicLink> </PublicLink>
</div> </div>
<div className={styles.objList}> <div className={styles.objList}>

View file

@ -30,11 +30,11 @@ const STEPS_V3: readonly StepV3[] = [
}, },
{ {
title: "Мы собираем рынок вокруг", title: "Мы собираем рынок вокруг",
text: "Похожие квартиры рядом: сколько их продаётся сейчас и за сколько дней они уходят.", text: "Похожие квартиры рядом: сколько их продаётся сейчас и как давно они висят.",
}, },
{ {
title: "Вы получаете цену и срок", title: "Вы получаете цену и картину рынка",
text: "Конкретная цифра для объявления, прогноз срока продажи и все аналоги, на которых построен расчёт.", text: "Конкретная цифра для объявления, сколько уже висят похожие объявления и все аналоги, на которых построен расчёт.",
accent: true, accent: true,
}, },
] as const; ] as const;

View file

@ -14,6 +14,14 @@
* компоненте так `StickyCtaV3` остаётся самодостаточным независимо от * компоненте так `StickyCtaV3` остаётся самодостаточным независимо от
* того, куда именно в дерево страницы его вставят (ожидаемое место сразу * того, куда именно в дерево страницы его вставят (ожидаемое место сразу
* после подвала, как Header/Footer в текущем `layout.tsx`). * после подвала, как Header/Footer в текущем `layout.tsx`).
*
* НА КНОПКЕ НЕТ СРОКА. Здесь стояло «Проверить за 30 секунд» замера времени
* заполнения формы нет и не было (та же причина, по которой «30 СЕКУНД» ушло
* из бейджа первого шага, см. `landing-facts.ts`). Панель висит на экране
* ВСЕГДА, поэтому именно с неё обещание, которого никто не мерил, читалось
* чаще всего. В тексте панели по той же причине нет и «срока продажи»:
* платный отчёт отдаёт медиану экспозиции похожих объявлений, а не прогноз
* срока (`_estimate_days_on_market`, см. HeroV3).
*/ */
import styles from "../../landing-v3.module.css"; import styles from "../../landing-v3.module.css";
@ -26,10 +34,10 @@ export function StickyCtaV3() {
<div className={styles.barSpacer} aria-hidden="true" /> <div className={styles.barSpacer} aria-hidden="true" />
<div className={styles.barRoot}> <div className={styles.barRoot}>
<p className={styles.barText}> <p className={styles.barText}>
Узнайте цену и срок продажи своей квартиры Узнайте цену своей квартиры и что происходит с похожими рядом
</p> </p>
<PublicLink to="estimate" className={styles.barBtn}> <PublicLink to="estimate" className={styles.barBtn}>
Проверить за 30 секунд Проверить квартиру
</PublicLink> </PublicLink>
</div> </div>
</> </>

View file

@ -13,6 +13,12 @@
* Цена `SERVICE_PRICE_RUB` (content.ts): один источник с офертой и политикой * Цена `SERVICE_PRICE_RUB` (content.ts): один источник с офертой и политикой
* возврата. Раньше она приезжала сюда из плейсхолдерной плитки «цена ошибки», * возврата. Раньше она приезжала сюда из плейсхолдерной плитки «цена ошибки»,
* и разъехаться с офертой могла молча. * и разъехаться с офертой могла молча.
*
* СОСТАВ ОТЧЁТА НАЗВАН ПО ФАКТУ. В перечне пути 1 стоял «срок»: платный отчёт
* действительно отдаёт `est_days_on_market`, но это медиана `days_on_market`
* аналогов сколько похожие объявления УЖЕ висят, а не срок до сделки
* (`deals.days_on_market` заполнен 0 раз из 108 623, сверять прогноз срока не
* с чем). Слово заменено на измеренную величину.
*/ */
import { SERVICE_PRICE_RUB, SUPPORT_TELEGRAM_URL } from "../../content"; import { SERVICE_PRICE_RUB, SUPPORT_TELEGRAM_URL } from "../../content";
@ -38,7 +44,8 @@ export function TwoPathsV3() {
<div className={styles.pathEyebrow}>ПУТЬ 1 · САМОСТОЯТЕЛЬНО</div> <div className={styles.pathEyebrow}>ПУТЬ 1 · САМОСТОЯТЕЛЬНО</div>
<div className={styles.pathTitle}>{`Отчёт за ${SERVICE_PRICE_RUB}`}</div> <div className={styles.pathTitle}>{`Отчёт за ${SERVICE_PRICE_RUB}`}</div>
<p className={styles.pathBody}> <p className={styles.pathBody}>
Цена, срок, аналоги, факторы. Дальше вы ведёте продажу сами на Цена, экспозиция похожих объявлений, аналоги, факторы. Дальше вы
ведёте продажу сами на
руках все цифры, чтобы уверенно держать свою цену в переговорах. руках все цифры, чтобы уверенно держать свою цену в переговорах.
</p> </p>
<PublicLink to="estimate" className={styles.pathBtnLight}> <PublicLink to="estimate" className={styles.pathBtnLight}>

View file

@ -149,8 +149,8 @@ export const HOWTO = {
}, },
cta: { cta: {
eyebrow: "ЕСЛИ СЧИТАТЬ ВРУЧНУЮ НЕКОГДА", eyebrow: "ЕСЛИ СЧИТАТЬ ВРУЧНУЮ НЕКОГДА",
title: "МЕРА делает всё это за 30 секунд", title: "МЕРА делает всё это за вас",
body: `Мы находим аналоги, считаем медиану, применяем поправки и проверяем цену сроком продажи. Бесплатно покажем рынок вокруг, за ${SERVICE_PRICE_RUB} ₽ — вашу цену и прогноз.`, body: `Мы находим аналоги, считаем медиану, применяем поправки и смотрим, сколько похожие объявления уже висят. Бесплатно покажем рынок вокруг, за ${SERVICE_PRICE_RUB} ₽ — вашу цену и аналоги, на которых она построена.`,
}, },
s5: { s5: {
id: "s5", id: "s5",

View file

@ -124,7 +124,7 @@ export default function HowToArticlePage() {
<div className={styles.artCtaTitle}>{HOWTO.cta.title}</div> <div className={styles.artCtaTitle}>{HOWTO.cta.title}</div>
<p className={styles.artCtaBody}>{HOWTO.cta.body}</p> <p className={styles.artCtaBody}>{HOWTO.cta.body}</p>
<PublicLink to="estimate" className={styles.gameBtnAccent}> <PublicLink to="estimate" className={styles.gameBtnAccent}>
Проверить за 30 секунд Проверить квартиру
</PublicLink> </PublicLink>
</div> </div>

View file

@ -88,7 +88,7 @@ export default function ArticlesPage() {
</p> </p>
</div> </div>
<PublicLink to="estimate" className={styles.gameBtnAccent}> <PublicLink to="estimate" className={styles.gameBtnAccent}>
Проверить за 30 секунд Проверить квартиру
</PublicLink> </PublicLink>
</div> </div>
</main> </main>

View file

@ -87,8 +87,13 @@ export const BACKTEST_CORRIDOR_LABEL = "±37 %";
export const BACKTEST_PERIOD_LABEL = "сделки с июня 2025 года"; export const BACKTEST_PERIOD_LABEL = "сделки с июня 2025 года";
/** /**
* Бейдж первого шага. Раньше здесь стояло «30 секунд» замера времени * Бейдж первого шага И подпись под кнопкой формы (`FreeCheckCard.tsx`).
* заполнения формы нет и не было. Число полей формы, наоборот, проверяется * Раньше в обоих местах стояло «30 секунд» замера времени заполнения формы
* глазами в `FreeCheckCard.tsx`: адрес, площадь, комнаты, этаж, состояние. * нет и не было. Число полей формы, наоборот, проверяется глазами там же:
* адрес, площадь, комнаты, этаж, состояние.
*
* Оба места берут строку ОТСЮДА, а не пишут её у себя: «30 секунд» пережило
* снятие из бейджа ровно потому, что было вписано в четыре компонента
* независимо, и правка одного не касалась остальных.
*/ */
export const STEP1_FIELDS_LABEL = "5 полей"; export const STEP1_FIELDS_LABEL = "5 полей";