fix(mera/b2c): снять с витрины обещание времени и подписать величины измеренным
All checks were successful
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 Trade-In / frontend-checks (pull_request) Successful in 57s
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI / openapi-codegen-check (pull_request) Has been skipped
All checks were successful
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 Trade-In / frontend-checks (pull_request) Successful in 57s
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI / openapi-codegen-check (pull_request) Has been skipped
«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:
parent
b17a2a310c
commit
2e82b0810f
16 changed files with 173 additions and 49 deletions
|
|
@ -11,6 +11,7 @@ import { describe, expect, it } from "vitest";
|
|||
|
||||
import { AccuracyV3 } from "../_components/v3/AccuracyV3";
|
||||
import { CostOfErrorV3 } from "../_components/v3/CostOfErrorV3";
|
||||
import { HeroV3 } from "../_components/v3/HeroV3";
|
||||
import type { LandingStat, ShowcaseResponse } from "../public-api";
|
||||
|
||||
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 дней среди снижавших"),
|
||||
};
|
||||
|
||||
/** Hero печатает ещё и медиану аналогов — величину БЕЗ фильтра по городу. */
|
||||
const HERO_STATS = {
|
||||
...STATS,
|
||||
analogs_median: stat(
|
||||
14,
|
||||
1123,
|
||||
"Медиана числа аналогов на расчёт (только расчёты, где аналоги нашлись)",
|
||||
),
|
||||
};
|
||||
|
||||
const SHOWCASE: ShowcaseResponse = {
|
||||
computed_at: "2026-08-29T03:00:00+00:00",
|
||||
deals: [
|
||||
|
|
@ -88,7 +99,35 @@ describe("витрина лэндинга v3 без данных", () => {
|
|||
|
||||
it("«Цена ошибки»: без /stats остаётся только цена — она из оферты, а не из замера", () => {
|
||||
render(<CostOfErrorV3 stats={{}} />);
|
||||
expect(screen.queryByText(/столько продавцов снижают цену/)).toBeNull();
|
||||
expect(screen.queryByText(/столько объявлений на Домклике снижали цену/)).toBeNull();
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { PublicLink } from "./PublicLink";
|
|||
* (`v3/HeaderV3.tsx`).
|
||||
*
|
||||
* Из неё убраны навигация по секциям лэндинга, селектор города и CTA
|
||||
* «Проверить за 30 секунд». Причина у всех трёх одна: человек уже ушёл с
|
||||
* «Проверить квартиру». Причина у всех трёх одна: человек уже ушёл с
|
||||
* лэндинга. Якоря вроде `#accuracy` тут просто не на что навести (секций нет
|
||||
* на этих страницах), город выбирается прямо в форме проверки — а второй
|
||||
* селектор города в шапке означал бы два места, задающих одно значение.
|
||||
|
|
|
|||
|
|
@ -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 styles from "../../landing-v3.module.css";
|
||||
|
||||
|
|
@ -129,7 +142,7 @@ function FreeCards({ verdict }: { verdict: CoverageVerdict }) {
|
|||
|
||||
const PAID_ROWS = [
|
||||
{ label: "РЕКОМЕНДОВАННАЯ ЦЕНА", w: "60%" },
|
||||
{ label: "ПРОГНОЗ СРОКА ПРОДАЖИ ПРИ ЭТОЙ ЦЕНЕ", w: "45%" },
|
||||
{ label: "СКОЛЬКО УЖЕ ВИСЯТ ПОХОЖИЕ ОБЪЯВЛЕНИЯ", w: "45%" },
|
||||
{ label: "ДИАПАЗОН И ПОЛОЖЕНИЕ НА РЫНКЕ", w: "70%" },
|
||||
{ label: "АНАЛОГИ С АДРЕСАМИ И ЦЕНАМИ", w: "88%" },
|
||||
] as const;
|
||||
|
|
@ -139,7 +152,9 @@ function PaidTeaser() {
|
|||
return (
|
||||
<div className={styles.repPaid}>
|
||||
<div className={styles.repPaidBar}>
|
||||
<span className={styles.repPaidBarText}>ПОЛНЫЙ ОТЧЁТ · 150 ₽</span>
|
||||
<span className={styles.repPaidBarText}>
|
||||
ПОЛНЫЙ ОТЧЁТ · {SERVICE_PRICE_RUB} ₽
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.repPaidGrid}>
|
||||
<div className={styles.repPaidLeft}>
|
||||
|
|
@ -159,17 +174,17 @@ function PaidTeaser() {
|
|||
</div>
|
||||
<div className={styles.repPaidRight}>
|
||||
<div className={styles.repPaidTitle}>
|
||||
Цена и срок продажи — следующий шаг
|
||||
Ваша цена — следующий шаг
|
||||
</div>
|
||||
<p className={styles.repPaidText}>
|
||||
Полный расчёт стоит 150 ₽: диапазон цены, цена за квадратный
|
||||
метр и список квартир, на которых он построен. Один платёж, без
|
||||
подписки. Приём оплаты сейчас подключается — как только заработает,
|
||||
кнопка появится здесь.
|
||||
Полный расчёт стоит {SERVICE_PRICE_RUB} ₽: диапазон цены, цена
|
||||
за квадратный метр и список квартир, на которых он построен. Один
|
||||
платёж, без подписки. Приём оплаты сейчас подключается — как только
|
||||
заработает, кнопка появится здесь.
|
||||
</p>
|
||||
<p className={styles.repPaidFine}>
|
||||
Если надёжный расчёт не получится — вернём 150 ₽:{" "}
|
||||
<PublicLink to="refund">условия возврата</PublicLink>.
|
||||
Если надёжный расчёт не получится — вернём {SERVICE_PRICE_RUB}
|
||||
₽: <PublicLink to="refund">условия возврата</PublicLink>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,13 @@
|
|||
* снижал»: одна подпись на две величины была бы подменой знаменателя.
|
||||
* Цена — `SERVICE_PRICE_RUB` (content.ts), тот же источник, что у оферты.
|
||||
*
|
||||
* ОБЪЯВЛЕНИЯ, А НЕ ПРОДАВЦЫ. `price_cut_share_pct` — доля ОБЪЯВЛЕНИЙ, и
|
||||
* только по Домклику (единственный источник, где триггер пишет стартовую
|
||||
* цену, см. шапку `landing_stats.py`). Один продавец ведёт несколько
|
||||
* объявлений, поэтому «столько продавцов снижают цену» приписывало величине
|
||||
* единицу, которой её не мерили: подпись приведена к измеренному, а источник
|
||||
* назван прямо в ней, а не только в `note` под плиткой.
|
||||
*
|
||||
* ЧЕГО ЗДЕСЬ БОЛЬШЕ НЕТ. Плитки «×2,4 дольше продаётся квартира с завышенной
|
||||
* ценой»: замер даёт ×1,04 (79 дней против 76), и это уже с цензурой в пользу
|
||||
* заявления. Её место заняла доля снижающих цену — измеренная величина про то
|
||||
|
|
@ -51,7 +58,7 @@ export function CostOfErrorV3({ stats }: { stats: LandingStats }) {
|
|||
tiles.push({
|
||||
key: "share",
|
||||
value: share.text,
|
||||
title: "столько продавцов снижают цену",
|
||||
title: "столько объявлений на Домклике снижали цену",
|
||||
text: [share.note, share.sample].filter(Boolean).join(" · "),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* FreeCheckCard — карточка «Бесплатная проверка» на первом экране (макет v3,
|
||||
* `МЕРА для себя v3.dc.html` строки ~129-170).
|
||||
*
|
||||
* ЧТО ОНА ДЕЛАЕТ СЕГОДНЯ. Проверяет пять полей и уводит на `/estimate`, где
|
||||
* ЧТО ОНА ДЕЛАЕТ СЕГОДНЯ. Проверяет шесть полей и уводит на `/estimate`, где
|
||||
* живёт сама проверка. Макетный сценарий «спиннер 1100 мс → готовый
|
||||
* результат прямо в карточке» не воспроизводится и не будет: результату нужно
|
||||
* место рядом с полями, а автокомплиту — место под список; в карточке первого
|
||||
|
|
@ -39,6 +39,11 @@
|
|||
* не использует (см. `CoverageProbeInput` на бэке), но выбросить уже
|
||||
* полученный ответ и спросить его второй раз на платном шаге — хуже, чем
|
||||
* донести его в `sessionStorage` до того шага.
|
||||
*
|
||||
* ПОДПИСЬ ПОД КНОПКОЙ. В ней стояло «30 СЕКУНД» — замера времени заполнения
|
||||
* формы нет и не было. Его место занял `STEP1_FIELDS_LABEL`: число полей —
|
||||
* проверяемый факт, и оно приезжает из `landing-facts.ts`, а не вписано сюда
|
||||
* второй раз (иначе с бейджем шага 1 они разъехались бы молча).
|
||||
*/
|
||||
|
||||
import { useId, useRef, useState } from "react";
|
||||
|
|
@ -54,6 +59,7 @@ import {
|
|||
SECONDARY_CITIES,
|
||||
} from "../../content";
|
||||
import { saveDraft } from "../../estimate-draft";
|
||||
import { STEP1_FIELDS_LABEL } from "../../landing-facts";
|
||||
import styles from "../../landing-v3.module.css";
|
||||
|
||||
/** Значение <option> «моего города нет в списке» (то же, что в v1-форме). */
|
||||
|
|
@ -359,7 +365,7 @@ export function FreeCheckCard() {
|
|||
Показать данные по моему дому →
|
||||
</button>
|
||||
<p className={styles.heroSubmitCaption}>
|
||||
БЕЗ РЕГИСТРАЦИИ · БЕЗ ЗВОНКОВ · 30 СЕКУНД
|
||||
{`БЕЗ РЕГИСТРАЦИИ · БЕЗ ЗВОНКОВ · ${STEP1_FIELDS_LABEL.toUpperCase()}`}
|
||||
</p>
|
||||
|
||||
{/* Живая область держим в DOM постоянно — регион, добавленный в
|
||||
|
|
|
|||
|
|
@ -32,9 +32,14 @@ export interface FreeResultV3Props {
|
|||
analogs: string;
|
||||
/** Подпись под числом объявлений. */
|
||||
analogsLabel: string;
|
||||
/** Средний срок продажи — крупная цифра второй плитки, напр. "47 дн.". */
|
||||
/**
|
||||
* Экспозиция похожих объявлений — крупная цифра второй плитки, напр.
|
||||
* "47 дн.". НЕ срок продажи: считается по тем, кто ещё висит, и снятие
|
||||
* объявления не означает сделку (см. `median_listing_age_days` в
|
||||
* public-api.ts).
|
||||
*/
|
||||
days: string;
|
||||
/** Подпись под сроком продажи. */
|
||||
/** Подпись под экспозицией. */
|
||||
daysLabel: string;
|
||||
/** Клик по «Проверить другую квартиру» — вернуть карточку к форме. */
|
||||
onBack: () => void;
|
||||
|
|
@ -72,7 +77,7 @@ export function FreeResultV3({
|
|||
</div>
|
||||
<div className={styles.heroResultOverlay}>
|
||||
<p className={styles.heroResultOverlayTitle}>
|
||||
Ваша цена и срок продажи готовы
|
||||
Ваша цена готова
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export function HeaderV3() {
|
|||
Для бизнеса
|
||||
</a>
|
||||
<a href="#check" className={styles.hdrCta}>
|
||||
Проверить за 30 секунд
|
||||
Проверить квартиру
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,20 @@
|
|||
* конкретного человека. Цена — `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 строки без полей исхода), и обещать его
|
||||
|
|
@ -53,12 +67,22 @@ export function HeroV3({ stats }: { stats: LandingStats }) {
|
|||
digits: 0,
|
||||
sampleWord: "объявлениям",
|
||||
});
|
||||
const marketExample = [
|
||||
analogs && `медиана аналогов на расчёт — ${analogs.text}${analogs.sample ? ` (${analogs.sample})` : ""}`,
|
||||
age && `активное объявление висит ${age.text}${age.sample ? ` (медиана ${age.sample})` : ""}`,
|
||||
]
|
||||
.filter((line): line is string => Boolean(line))
|
||||
.join("; ");
|
||||
const marketLines = [
|
||||
age && {
|
||||
key: "age",
|
||||
label: "Пример по рынку Екатеринбурга, а не расчёт по вашему адресу:",
|
||||
text: [`активное объявление висит ${age.text}`, age.sample && `медиана ${age.sample}`]
|
||||
.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 (
|
||||
<section
|
||||
|
|
@ -99,8 +123,8 @@ export function HeroV3({ stats }: { stats: LandingStats }) {
|
|||
→
|
||||
</span>
|
||||
<span className={styles.heroListText}>
|
||||
<strong>Не зависнуть:</strong> узнаете, за сколько дней уйдёт
|
||||
квартира по вашей цене
|
||||
<strong>Не зависнуть:</strong> увидите, сколько времени уже
|
||||
висят похожие квартиры рядом
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.heroListItem}>
|
||||
|
|
@ -133,20 +157,20 @@ export function HeroV3({ stats }: { stats: LandingStats }) {
|
|||
За{" "}
|
||||
<span className={styles.heroNoBreak}>{`${SERVICE_PRICE_RUB} ₽`}</span>:
|
||||
</strong>{" "}
|
||||
ваша цена, срок продажи и аналоги, на которых построен расчёт
|
||||
ваша цена, экспозиция похожих объявлений и аналоги, на которых
|
||||
построен расчёт
|
||||
</span>
|
||||
</div>
|
||||
{marketExample !== "" && (
|
||||
<div className={styles.heroListItem}>
|
||||
{marketLines.map((line) => (
|
||||
<div key={line.key} className={styles.heroListItem}>
|
||||
<span className={styles.heroListIcon} aria-hidden="true">
|
||||
≈
|
||||
</span>
|
||||
<span className={styles.heroListText}>
|
||||
<strong>Пример по рынку Екатеринбурга, а не расчёт по вашему адресу:</strong>{" "}
|
||||
{marketExample}
|
||||
<strong>{line.label}</strong> {line.text}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Покрытие подано неравномерно намеренно — см. COVERED_CITIES в
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ export function ObjectionsV3() {
|
|||
Что обычно спрашивают перед оплатой
|
||||
</h2>
|
||||
<PublicLink to="estimate" className={styles.objCtaBtn}>
|
||||
Проверить за 30 секунд
|
||||
Проверить квартиру
|
||||
</PublicLink>
|
||||
</div>
|
||||
<div className={styles.objList}>
|
||||
|
|
|
|||
|
|
@ -30,11 +30,11 @@ const STEPS_V3: readonly StepV3[] = [
|
|||
},
|
||||
{
|
||||
title: "Мы собираем рынок вокруг",
|
||||
text: "Похожие квартиры рядом: сколько их продаётся сейчас и за сколько дней они уходят.",
|
||||
text: "Похожие квартиры рядом: сколько их продаётся сейчас и как давно они висят.",
|
||||
},
|
||||
{
|
||||
title: "Вы получаете цену и срок",
|
||||
text: "Конкретная цифра для объявления, прогноз срока продажи и все аналоги, на которых построен расчёт.",
|
||||
title: "Вы получаете цену и картину рынка",
|
||||
text: "Конкретная цифра для объявления, сколько уже висят похожие объявления и все аналоги, на которых построен расчёт.",
|
||||
accent: true,
|
||||
},
|
||||
] as const;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,14 @@
|
|||
* компоненте — так `StickyCtaV3` остаётся самодостаточным независимо от
|
||||
* того, куда именно в дерево страницы его вставят (ожидаемое место — сразу
|
||||
* после подвала, как Header/Footer в текущем `layout.tsx`).
|
||||
*
|
||||
* НА КНОПКЕ НЕТ СРОКА. Здесь стояло «Проверить за 30 секунд» — замера времени
|
||||
* заполнения формы нет и не было (та же причина, по которой «30 СЕКУНД» ушло
|
||||
* из бейджа первого шага, см. `landing-facts.ts`). Панель висит на экране
|
||||
* ВСЕГДА, поэтому именно с неё обещание, которого никто не мерил, читалось
|
||||
* чаще всего. В тексте панели по той же причине нет и «срока продажи»:
|
||||
* платный отчёт отдаёт медиану экспозиции похожих объявлений, а не прогноз
|
||||
* срока (`_estimate_days_on_market`, см. HeroV3).
|
||||
*/
|
||||
|
||||
import styles from "../../landing-v3.module.css";
|
||||
|
|
@ -26,10 +34,10 @@ export function StickyCtaV3() {
|
|||
<div className={styles.barSpacer} aria-hidden="true" />
|
||||
<div className={styles.barRoot}>
|
||||
<p className={styles.barText}>
|
||||
Узнайте цену и срок продажи своей квартиры
|
||||
Узнайте цену своей квартиры и что происходит с похожими рядом
|
||||
</p>
|
||||
<PublicLink to="estimate" className={styles.barBtn}>
|
||||
Проверить за 30 секунд
|
||||
Проверить квартиру
|
||||
</PublicLink>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@
|
|||
* Цена — `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";
|
||||
|
|
@ -38,7 +44,8 @@ export function TwoPathsV3() {
|
|||
<div className={styles.pathEyebrow}>ПУТЬ 1 · САМОСТОЯТЕЛЬНО</div>
|
||||
<div className={styles.pathTitle}>{`Отчёт за ${SERVICE_PRICE_RUB} ₽`}</div>
|
||||
<p className={styles.pathBody}>
|
||||
Цена, срок, аналоги, факторы. Дальше вы ведёте продажу сами — на
|
||||
Цена, экспозиция похожих объявлений, аналоги, факторы. Дальше вы
|
||||
ведёте продажу сами — на
|
||||
руках все цифры, чтобы уверенно держать свою цену в переговорах.
|
||||
</p>
|
||||
<PublicLink to="estimate" className={styles.pathBtnLight}>
|
||||
|
|
|
|||
|
|
@ -149,8 +149,8 @@ export const HOWTO = {
|
|||
},
|
||||
cta: {
|
||||
eyebrow: "ЕСЛИ СЧИТАТЬ ВРУЧНУЮ НЕКОГДА",
|
||||
title: "МЕРА делает всё это за 30 секунд",
|
||||
body: `Мы находим аналоги, считаем медиану, применяем поправки и проверяем цену сроком продажи. Бесплатно покажем рынок вокруг, за ${SERVICE_PRICE_RUB} ₽ — вашу цену и прогноз.`,
|
||||
title: "МЕРА делает всё это за вас",
|
||||
body: `Мы находим аналоги, считаем медиану, применяем поправки и смотрим, сколько похожие объявления уже висят. Бесплатно покажем рынок вокруг, за ${SERVICE_PRICE_RUB} ₽ — вашу цену и аналоги, на которых она построена.`,
|
||||
},
|
||||
s5: {
|
||||
id: "s5",
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ export default function HowToArticlePage() {
|
|||
<div className={styles.artCtaTitle}>{HOWTO.cta.title}</div>
|
||||
<p className={styles.artCtaBody}>{HOWTO.cta.body}</p>
|
||||
<PublicLink to="estimate" className={styles.gameBtnAccent}>
|
||||
Проверить за 30 секунд
|
||||
Проверить квартиру
|
||||
</PublicLink>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ export default function ArticlesPage() {
|
|||
</p>
|
||||
</div>
|
||||
<PublicLink to="estimate" className={styles.gameBtnAccent}>
|
||||
Проверить за 30 секунд
|
||||
Проверить квартиру
|
||||
</PublicLink>
|
||||
</div>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -87,8 +87,21 @@ export const BACKTEST_CORRIDOR_LABEL = "±37 %";
|
|||
export const BACKTEST_PERIOD_LABEL = "сделки с июня 2025 года";
|
||||
|
||||
/**
|
||||
* Бейдж первого шага. Раньше здесь стояло «30 секунд» — замера времени
|
||||
* заполнения формы нет и не было. Число полей формы, наоборот, проверяется
|
||||
* глазами в `FreeCheckCard.tsx`: адрес, площадь, комнаты, этаж, состояние.
|
||||
* Бейдж первого шага И подпись под кнопкой формы (`FreeCheckCard.tsx`).
|
||||
* Раньше в обоих местах стояло «30 секунд» — замера времени заполнения формы
|
||||
* нет и не было. Число полей формы, наоборот, проверяется глазами там же:
|
||||
* город, адрес, площадь, комнаты, этаж, состояние.
|
||||
*
|
||||
* ШЕСТЬ, А НЕ ПЯТЬ. Пока эта подпись делалась, соседняя ветка вернула в форму
|
||||
* ВЫБОР ГОРОДА (#2576: без него «Ленина, 1» давало жителю Нижнего Тагила цену
|
||||
* по одноимённой улице в ЕКБ). Число полей — не константа продукта, а счёт
|
||||
* того, что стоит в разметке рядом; после слияния двух веток «5 полей» было бы
|
||||
* ровно тем враньём на витрине, ради снятия которого подпись и заводилась.
|
||||
* Меняется форма — пересчитать здесь; проверяется по `heroFieldLabel` в
|
||||
* `FreeCheckCard.tsx`.
|
||||
*
|
||||
* Оба места берут строку ОТСЮДА, а не пишут её у себя: «30 секунд» пережило
|
||||
* снятие из бейджа ровно потому, что было вписано в четыре компонента
|
||||
* независимо, и правка одного не касалась остальных.
|
||||
*/
|
||||
export const STEP1_FIELDS_LABEL = "5 полей";
|
||||
export const STEP1_FIELDS_LABEL = "6 полей";
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue