fix(mera/b2c): вернуть потерянные вопросы FAQ, починить адрес превью и честность двух комментариев
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 1m0s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 1m0s
Правки по трём ревью ветки «v3 становится корнем».
FAQ. Из шести вопросов `content.ts` возражения v3 брали два, остальные
четыре (`how-accurate`, `vs-marketplace`, `personal-data`, `bank-report`)
не рендерились нигде: `Faq.tsx`, который выводил все шесть, удалён вместе
с лэндингом v1. Макетные возражения их тематически закрывали, но
пересказом — а оригиналы проходили юр-ревью. Содержательно пропали два
утверждения: «Имя, паспорт и документы на квартиру мы не спрашиваем» с
отсылкой к странице ПДн (152-ФЗ) и «показываем диапазон, а не одно число»
как прямой ответ про точность (рамка диапазона осталась только в
`AccuracyV3`, рядом с плейсхолдерными числами).
Теперь в секции один порядок чтения: все шесть вопросов из `FAQ` по id
плюс три макетных возражения, которым в `FAQ` пары нет («факт сделки»,
«почему так дёшево», «подписка»). Три макетных пересказа удалены в пользу
оригиналов; у «почему так дёшево» убран второй абзац — почти дословный
повтор второго абзаца `how-accurate`, который теперь стоит следующим.
Новых формулировок не добавлено. Отсылка к странице ПДн стала живой
ссылкой через `PublicLink` (обычный `<a>`, basePath не течёт); текст
ссылки — слова самого `content.ts`, найденные по вхождению.
Гейт стал двусторонним: тест перебирает `FAQ` и требует, чтобы КАЖДЫЙ id
был отрисован. Прежняя версия перечисляла два id руками и потому молчала
ровно в тот момент, когда четыре ответа исчезли со страницы. Проверено
мутацией: снятие вопроса, переименование id и переформулировка фразы про
ПДн красят тест по отдельности.
CADDY. `/trade-in/mera-public/v3` выбыл из @meraLongPages вместе со
страницей и никуда не попал — длинный адрес превью падал в catch-all 404,
хотя раньше вёл на страницу. Обе его формы добавлены в @meraV3Gone. Там же
редирект перестал терять query: было `redir * /`, стало `redir * {uri}`
со срезанием пути — как у @meraLongPages, где перенос UTM и обоснован.
Проверено на живом Caddy 2.11 по настоящему site-блоку: все четыре формы
дают 301 на `/` с сохранённой query, без параметров — чистый `/` без
хвоста `?`, соседние матчеры не задеты, лишний сегмент по-прежнему 404.
КОММЕНТАРИИ, ОПИСЫВАЮЩИЕ НЕСУЩЕСТВУЮЩЕЕ. Строка `page.tsx` в гейте
плейсхолдеров убрана: комментарий над ней утверждал, что устаревший путь
«перестал бы что-либо сканировать», но ни в корневом `page.tsx`, ни в
прежнем `v3/page.tsx` нет ни одного имени из `PLACEHOLDER_EXPORTS` —
страница только собирает секции. Замер подтверждает: множество найденных
плейсхолдеров со строкой и без неё совпадает, а сама по себе она не даёт
ничего. Держать её значило держать ложное ощущение охвата.
Дескриптор «Оценка вторичного жилья по рыночным данным · <регион>» на
внутренние страницы не возвращается — причина записана в докстринге
`InnerHeader`: эту шапку носят и `/estimate`, `/articles`, `/docs`, где
дескриптора не было никогда; требование юриста было про первый экран
лэндинга и там выполняется `HeroV3`; на самих юр-страницах то же сказано
сильнее — в тексте оферты (п. 1.3, 6.2) и в `FooterV3` на каждой странице.
This commit is contained in:
parent
6976ca8915
commit
49652f4413
7 changed files with 242 additions and 68 deletions
|
|
@ -372,12 +372,35 @@ meraocenka.ru {
|
|||
# двух недель: владелец сравнивал по нему версии, и ссылка почти наверняка
|
||||
# осталась в закладках и в переписке. Поэтому 301 на корень, а не молчание
|
||||
# белого списка: человек по старой ссылке попадает ровно на то, что по ней
|
||||
# и смотрел, только теперь это главная. Обе формы — со слэшем и без:
|
||||
# `/v3/` вышел из @meraShortSlash вместе с остальным превью и иначе упал бы
|
||||
# в catch-all 404.
|
||||
@meraV3Gone path /v3 /v3/
|
||||
# и смотрел, только теперь это главная.
|
||||
#
|
||||
# ЧЕТЫРЕ ФОРМЫ, А НЕ ДВЕ. Превью раздавалось и коротким `/v3`, и длинным
|
||||
# `/trade-in/mera-public/v3` — вторая пара до 29.08.2026 жила в
|
||||
# @meraLongPages и оттуда выбыла вместе со страницей. Короткие формы её
|
||||
# не подхватывают (матчер `path` требует точного совпадения), поэтому
|
||||
# длинный адрес превью падал в catch-all 404 — тот же класс дефекта,
|
||||
# ради которого длинные адреса вообще оставлены живыми. Все четыре формы
|
||||
# ведут в один корень, поэтому и матчер один.
|
||||
#
|
||||
# QUERY ПЕРЕНОСИТСЯ — `redir * {uri}`, как в @meraLongPages, а не
|
||||
# `redir * /`, как было здесь до этой правки. Причина ровно та же, что
|
||||
# записана у @meraLongPages: по разосланной ссылке приходят с UTM-метками,
|
||||
# и `/v3?utm=…` не должен терять атрибуцию на редиректе. Оба
|
||||
# `uri strip_prefix` срезают путь целиком (для `/v3` — до пустого, для
|
||||
# `/v3/` — до `/`), а `{uri}` при пустом пути даёт `/`, поэтому запрос без
|
||||
# параметров отдаёт чистый `Location: /` без хвоста `?`. Обёртка `route`
|
||||
# обязательна по той же причине, что у @meraLongPages: внутри `handle`
|
||||
# порядок директив определяет Caddy, и без неё `redir` выполнится раньше
|
||||
# `uri`. Проверено на живом Caddy 2.11 (прод — `caddy:2`): все четыре формы
|
||||
# с параметрами и без дают 301 на `/` с сохранённой query, а лишний
|
||||
# сегмент (`/trade-in/mera-public/v3/evil`) по-прежнему падает в 404.
|
||||
@meraV3Gone path /v3 /v3/ /trade-in/mera-public/v3 /trade-in/mera-public/v3/
|
||||
handle @meraV3Gone {
|
||||
redir * / permanent
|
||||
route {
|
||||
uri strip_prefix /trade-in/mera-public
|
||||
uri strip_prefix /v3
|
||||
redir * {uri} permanent
|
||||
}
|
||||
}
|
||||
|
||||
# Длинные адреса поддерева → 301 на короткие. Один канонический адрес у
|
||||
|
|
|
|||
|
|
@ -34,13 +34,21 @@ function read(rel: string): string {
|
|||
return readFileSync(join(ROOT, rel), "utf8");
|
||||
}
|
||||
|
||||
/** Компоненты лэндинга v3, которые могут ссылаться на плейсхолдеры. */
|
||||
/**
|
||||
* Компоненты лэндинга v3, которые могут ссылаться на плейсхолдеры.
|
||||
*
|
||||
* САМОЙ СТРАНИЦЫ (`page.tsx`) ЗДЕСЬ НЕТ, И ЭТО НЕ ПРОПУСК. 29.08.2026 лэндинг
|
||||
* переехал из `v3/page.tsx` в корень, и путь в этом списке обновили вслед за
|
||||
* файлом — с обоснованием «иначе строка перестала бы что-либо сканировать».
|
||||
* Обоснование звучало как проверенный факт, но проверено не было: ни в
|
||||
* `page.tsx`, ни в прежнем `v3/page.tsx` нет ни одного имени из
|
||||
* `PLACEHOLDER_EXPORTS` — страница только собирает секции из импортов, а числа
|
||||
* живут в самих секциях. То есть строка не давала покрытия ни до переезда, ни
|
||||
* после, и держать её значило держать ложное ощущение охвата. Появится величина
|
||||
* прямо в разметке страницы — строку сюда вернуть (и тогда она будет что-то
|
||||
* значить).
|
||||
*/
|
||||
const V3_SOURCES = [
|
||||
// 29.08.2026: лэндинг v3 переехал из `v3/page.tsx` в корень. Путь обязан
|
||||
// ехать вместе с ним — `placeholdersInUse()` молча пропускает ненайденный
|
||||
// файл (это правильно для переименований), поэтому устаревшая строка не
|
||||
// покраснела бы, а просто перестала бы что-либо сканировать.
|
||||
"page.tsx",
|
||||
"_components/v3/HeroV3.tsx",
|
||||
"_components/v3/AccuracyV3.tsx",
|
||||
"_components/v3/FreeCheckCard.tsx",
|
||||
|
|
|
|||
|
|
@ -196,6 +196,11 @@ describe("короткие адреса публичного домена", () =
|
|||
*
|
||||
* Правильный ответ ровно один — 301 на корень, и он проверяется здесь, а не
|
||||
* держится на памяти о том, почему в конфиге появился ещё один матчер.
|
||||
*
|
||||
* ФОРМ ЧЕТЫРЕ, А НЕ ДВЕ. Превью раздавалось и коротким `/v3`, и длинным
|
||||
* `/trade-in/mera-public/v3`; вторая пара выбыла из @meraLongPages вместе со
|
||||
* страницей и никуда не попала — длинный адрес превью отдавал 404, ровно тот
|
||||
* же дефект, что проверка выше ловит для страниц.
|
||||
*/
|
||||
it("адрес выбывшего превью /v3 отдаёт 301 на корень, а не 404", () => {
|
||||
const matcher = meraBlock.match(/@meraV3Gone path ([^\n]+)/)?.[1] ?? "";
|
||||
|
|
@ -203,14 +208,32 @@ describe("короткие адреса публичного домена", () =
|
|||
expect(
|
||||
paths,
|
||||
"нет матчера @meraV3Gone — старые ссылки на превью упадут в catch-all 404",
|
||||
).toEqual(["/v3", "/v3/"]);
|
||||
).toEqual([
|
||||
"/v3",
|
||||
"/v3/",
|
||||
"/trade-in/mera-public/v3",
|
||||
"/trade-in/mera-public/v3/",
|
||||
]);
|
||||
|
||||
// Тело `handle` содержит вложенный `route { … }`, поэтому берём до
|
||||
// закрывающей скобки НА УРОВНЕ handle (четыре пробела), а не до первой.
|
||||
const handler =
|
||||
meraBlock.match(/handle @meraV3Gone \{([^}]*)\}/)?.[1] ?? "";
|
||||
meraBlock.match(/handle @meraV3Gone \{([\s\S]*?)\n {4}\}/)?.[1] ?? "";
|
||||
expect(
|
||||
handler,
|
||||
"матчер есть, а редиректа в нём нет — зонд выше прошёл бы вхолостую",
|
||||
).toMatch(/redir\s+\*\s+\/\s+permanent/);
|
||||
).toMatch(/redir\s+\*\s+\{uri\}\s+permanent/);
|
||||
/**
|
||||
* Query обязана переноситься — иначе `/v3?utm=…` теряет атрибуцию у уже
|
||||
* разосланных ссылок. Один `redir * {uri}` этого не гарантирует: без
|
||||
* срезания пути он вернул бы Location, равный исходному адресу (цикл).
|
||||
* Поэтому проверяется вся связка, а не только форма редиректа.
|
||||
*/
|
||||
expect(
|
||||
handler,
|
||||
"путь не срезается — {uri} вернёт исходный адрес, редирект закольцуется",
|
||||
).toContain("uri strip_prefix /v3");
|
||||
expect(handler).toContain("uri strip_prefix /trade-in/mera-public");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { FAQ } from "../content";
|
|||
* Новый дизайн жил на /v3 и 29.08.2026 стал корнем meraocenka.ru. Перед этим
|
||||
* разведка нашла блоки лэндинга v1, которых в v3 не было вовсе: реестр
|
||||
* источников, «что вы получите» с дисклеймером 135-ФЗ, плашка покрытия и
|
||||
* строка «мы не покупаем квартиры» в герое, два вопроса из FAQ, город в форме.
|
||||
* строка «мы не покупаем квартиры» в герое, вопросы FAQ, город в форме.
|
||||
* Они перенесены — здесь проверяется, что перенос ЖИВОЙ: секции подключены к
|
||||
* странице, а компоненты читают те же константы, а не копии строк.
|
||||
*
|
||||
|
|
@ -70,19 +70,62 @@ describe("секции читают константы, а не копии ст
|
|||
});
|
||||
});
|
||||
|
||||
describe("два вопроса из FAQ v1 живут в возражениях v3", () => {
|
||||
describe("КАЖДЫЙ вопрос FAQ живёт в возражениях v3", () => {
|
||||
const objections = read("_components/v3/ObjectionsV3.tsx");
|
||||
|
||||
/**
|
||||
* Проверка ДВУСТОРОННЯЯ: id должен и существовать в FAQ, и упоминаться в
|
||||
* секции. Односторонняя пропустила бы главную ошибку — переименование id, от
|
||||
* которого `FAQ.find` вернёт undefined, и вопрос молча исчезнет со страницы.
|
||||
* Список вопросов НЕ ЗАХАРДКОЖЕН — он перебирается из самого `FAQ`. В этом
|
||||
* вся разница с прежней версией проверки: она перечисляла два id вручную и
|
||||
* потому была зелёной ровно в тот момент, когда `Faq.tsx` (единственный, кто
|
||||
* рендерил все шесть) удалили вместе с лэндингом v1, а в возражения v3
|
||||
* перенесли только эти два. Четыре ответа, прошедшие юр-ревью, не
|
||||
* рендерились нигде, и ни один гейт этого не сказал.
|
||||
*
|
||||
* Перебор по `FAQ` краснеет в обе стороны:
|
||||
* - вопрос убрали из ObjectionsV3 → его id в файле нет → красный;
|
||||
* - id переименовали в content.ts → в файле остался СТАРЫЙ id, `FAQ.find`
|
||||
* молча вернёт undefined и вопрос исчезнет со страницы → красный;
|
||||
* - в content.ts добавили седьмой вопрос → он обязан быть либо отрисован,
|
||||
* либо осознанно исключён (тогда правится и этот тест), а не потерян.
|
||||
*
|
||||
* Ищем `...fromFaq("id")` со спредом, а не просто id: спред — это то, что
|
||||
* кладёт вопрос в `ITEMS`. Объявление без спреда собралось бы, тайпчек бы
|
||||
* промолчал, а на странице вопроса не было бы.
|
||||
*/
|
||||
it.each(["how-do-you-know", "why-region"])("%s", (id) => {
|
||||
it("в FAQ вообще есть вопросы — иначе перебор ниже пустой", () => {
|
||||
expect(FAQ.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it.each(FAQ.map((f) => f.id))("%s отрисован", (id) => {
|
||||
expect(
|
||||
FAQ.map((f) => f.id),
|
||||
`id пропал из FAQ — ObjectionsV3 отрендерит на один вопрос меньше и никто не заметит`,
|
||||
).toContain(id);
|
||||
expect(objections).toContain(`"${id}"`);
|
||||
objections,
|
||||
`вопрос "${id}" не попадает в ITEMS — на странице его не будет, и заметить это может только человек`,
|
||||
).toContain(`...fromFaq("${id}")`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Отсылка к странице про обработку персональных данных обязана быть ЖИВОЙ
|
||||
* ССЫЛКОЙ. В `content.ts` она лежит текстом, ObjectionsV3 линкует её по
|
||||
* дословному совпадению — значит переформулировка предложения в content.ts
|
||||
* тихо превращает ссылку обратно в текст. Проверка сверяет обе стороны.
|
||||
*/
|
||||
it("отсылка к странице ПДн отдаётся ссылкой, а не текстом", () => {
|
||||
const phrase =
|
||||
objections.match(/const PRIVACY_PHRASE = "([^"]+)"/)?.[1] ?? "";
|
||||
expect(
|
||||
phrase,
|
||||
"зонд сломан: в ObjectionsV3 не найдена константа связующей фразы",
|
||||
).not.toBe("");
|
||||
|
||||
const answer = FAQ.find((f) => f.id === "personal-data")?.a.join(" ") ?? "";
|
||||
expect(
|
||||
answer,
|
||||
"фраза в content.ts переформулирована — ссылка на страницу ПДн молча станет обычным текстом",
|
||||
).toContain(phrase);
|
||||
|
||||
expect(
|
||||
objections,
|
||||
"ссылка обязана идти через PublicLink (обычный <a>): next/link подставит basePath",
|
||||
).toContain('<PublicLink to="privacy"');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,32 @@ import { PublicLink } from "./PublicLink";
|
|||
*
|
||||
* Логотип отсюда — ссылка на главную: это единственный обратный путь на
|
||||
* лэндинг, и он же то, что человек инстинктивно нажимает.
|
||||
*
|
||||
* ДЕСКРИПТОРА «ОЦЕНКА ВТОРИЧНОГО ЖИЛЬЯ ПО РЫНОЧНЫМ ДАННЫМ · <РЕГИОН>» ЗДЕСЬ
|
||||
* НЕТ — И ЭТО РЕШЕНИЕ, А НЕ ПОТЕРЯ. Строка жила в `SiteHeader.tsx` (шапка
|
||||
* лэндинга v1) с формулировкой, о которой просил юрист 14.08.2026: слова «по
|
||||
* рыночным данным» — то, чем сервис отличается от оценки по 135-ФЗ, и они
|
||||
* должны стоять в заметном дескрипторе, а не только в дисклеймерах внизу.
|
||||
* 29.08.2026 `SiteHeader` удалён, юр-страницы переведены на эту шапку, и
|
||||
* строка с них ушла.
|
||||
*
|
||||
* Почему она сюда не возвращается:
|
||||
* - ЭТО ШАПКА НЕ ТОЛЬКО ЮР-СТРАНИЦ. Её же носят `/estimate`, `/articles`,
|
||||
* `/docs`, у которых дескриптора не было никогда: возврат «на место»
|
||||
* означал бы новую строку на трёх страницах, которых правка не касается.
|
||||
* - ТРЕБОВАНИЕ ЮРИСТА БЫЛО ПРО ЛЭНДИНГ — про первое, что видит посетитель.
|
||||
* На корне оно выполняется и сейчас, только не шапкой: `HeroV3` первым же
|
||||
* экраном пишет «„Мера“ — сервис оценки вторичного жилья по рыночным
|
||||
* данным», и там же стоит граница покрытия.
|
||||
* - НА САМИХ ЮР-СТРАНИЦАХ ЭТО СКАЗАНО СИЛЬНЕЕ, ЧЕМ В ДЕСКРИПТОРЕ. Оферта
|
||||
* говорит то же в тексте договора (п. 1.3 «индикативный расчёт
|
||||
* ориентировочной рыночной стоимости», п. 6.2 «не является отчётом об
|
||||
* оценке по 135-ФЗ»), а `FooterV3` на каждой из этих страниц несёт и
|
||||
* рыночную природу оценки с регионом («Оценка квартир на вторичном рынке
|
||||
* по сделкам и объявлениям. <регион>»), и прямой отказ («не является
|
||||
* офертой или отчётом об оценке»).
|
||||
* Появится страница, где ни то ни другое не выполняется, — дескриптор нужно
|
||||
* вернуть ей, а не этой общей шапке.
|
||||
*/
|
||||
export function InnerHeader() {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -12,17 +12,39 @@
|
|||
* объявлений с публикации), а не макетной формулировкой про «отметки
|
||||
* продавцов», механики которых не существует.
|
||||
*
|
||||
* ДВА ВОПРОСА ИЗ v1 (`FAQ` в content.ts) ИМПОРТИРУЮТСЯ, А НЕ ПЕРЕПИСАНЫ.
|
||||
* Макетные шесть возражений — про оплату и доверие; в v1 было два вопроса,
|
||||
* которых среди них нет вовсе, и оба несут границы продукта:
|
||||
* - `how-do-you-know` — как вообще получается цена именно этой квартиры;
|
||||
* - `why-region` — почему одна область, и чем «полное покрытие» отличается
|
||||
* от «данных меньше».
|
||||
* Первый стоит ПЕРВЫМ (это вопрос до всяких сомнений об оплате), второй —
|
||||
* последним (география — то, с чем человек уходит). Берём их из `FAQ` по id, а
|
||||
* не копией строк: копия разъедется с ответом на странице /docs и в v1, где
|
||||
* тот же текст уже прошёл юр-ревью. Если id переименуют, вопрос молча
|
||||
* исчезнет — это гейтится `__tests__/v3-ported-copy.test.ts`.
|
||||
* ВЕСЬ `FAQ` ИЗ content.ts ИМПОРТИРУЕТСЯ, А НЕ ПЕРЕПИСЫВАЕТСЯ. Так было не
|
||||
* сразу: при переносе брались только `how-do-you-know` и `why-region`, а
|
||||
* остальные четыре вопроса v1 (`how-accurate`, `vs-marketplace`,
|
||||
* `personal-data`, `bank-report`) не рендерились НИГДЕ — старый компонент
|
||||
* `Faq.tsx`, который выводил все шесть, удалён вместе с лэндингом v1.
|
||||
* Тематически их закрывали макетные возражения, но это были ПЕРЕСКАЗЫ, а
|
||||
* оригиналы проходили юр-ревью. Две потери были содержательными:
|
||||
* - `personal-data` — единственное место на сайте, где сказано «Имя, паспорт
|
||||
* и документы на квартиру мы не спрашиваем», и единственная отсылка к
|
||||
* странице про обработку персональных данных (152-ФЗ). Макетная версия
|
||||
* оставляла только «отдельную галочку согласия», без ссылки;
|
||||
* - `how-accurate` — «показываем диапазон, а не одно число» как прямой ответ
|
||||
* на вопрос о точности. После потери рамка диапазона осталась только в
|
||||
* `AccuracyV3`, рядом с плейсхолдерными числами.
|
||||
*
|
||||
* ЧТО СДЕЛАНО. Список ниже — один порядок чтения, в который вплетены и все
|
||||
* шесть вопросов `FAQ` (по id, а не копией строк), и те макетные возражения,
|
||||
* которым в `FAQ` пары нет вовсе: «откуда вы знаете факт сделки», «почему так
|
||||
* дёшево», «это подписка». Три остальных макетных возражения — про
|
||||
* калькуляторы, звонки и отчёт для банка — УДАЛЕНЫ как пересказы
|
||||
* `vs-marketplace` / `personal-data` / `bank-report`: держать рядом два ответа
|
||||
* на один вопрос хуже, чем один, а выбор между пересказом и выверенным
|
||||
* оригиналом очевиден. По той же причине у «почему так дёшево» убран второй
|
||||
* абзац — он был почти дословным повтором второго абзаца `how-accurate`,
|
||||
* который теперь стоит следующим вопросом и отвечает на «это точно работает?»
|
||||
* целиком.
|
||||
*
|
||||
* Ни одной строки не написано здесь заново: тексты либо приходят из
|
||||
* `content.ts`, либо это уже стоявшие в этом файле макетные возражения.
|
||||
*
|
||||
* Если id переименуют или вопрос выпадет из списка, он молча исчезнет со
|
||||
* страницы — гейтится `__tests__/v3-ported-copy.test.ts` двусторонне: тест
|
||||
* перебирает `FAQ` и требует, чтобы КАЖДЫЙ его id был отрисован здесь.
|
||||
*/
|
||||
|
||||
import { FAQ } from "../../content";
|
||||
|
|
@ -35,7 +57,20 @@ interface Objection {
|
|||
readonly a: readonly string[];
|
||||
}
|
||||
|
||||
const OBJECTIONS: readonly Objection[] = [
|
||||
/** Вопрос из `FAQ` по id — `[]`, если id переименовали (тест это ловит). */
|
||||
function fromFaq(id: string): readonly Objection[] {
|
||||
const item = FAQ.find((f) => f.id === id);
|
||||
return item ? [{ q: item.q, a: item.a }] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Порядок чтения: как получается цена → откуда факт → сколько стоит и почему
|
||||
* столько → насколько точно → чем отличается от бесплатного → как списывают
|
||||
* деньги → что с данными → годится ли для банка → почему одна область.
|
||||
* Сомнения про деньги стоят посередине, границы продукта — по краям.
|
||||
*/
|
||||
const ITEMS: readonly Objection[] = [
|
||||
...fromFaq("how-do-you-know"),
|
||||
{
|
||||
q: "Откуда вы знаете факт сделки?",
|
||||
a: [
|
||||
|
|
@ -46,48 +81,50 @@ const OBJECTIONS: readonly Objection[] = [
|
|||
q: "Почему так дёшево — это точно работает?",
|
||||
a: [
|
||||
"Дёшево, потому что расчёт автоматический: без выезда, без оценщика, без комиссии. Вы платите за данные, а не за часы специалиста.",
|
||||
"Точность прямо зависит от того, сколько нашлось сопоставимых объектов, — поэтому мы всегда пишем, на скольких объектах и сделках построен расчёт, и честно сообщаем, если данных по дому мало.",
|
||||
],
|
||||
},
|
||||
{
|
||||
q: "Чем это лучше бесплатных калькуляторов?",
|
||||
a: [
|
||||
"Калькулятор площадки считает по объявлениям этой же площадки — то есть по ценам, которые продавцы просят, а не получают.",
|
||||
"Мы смотрим сразу несколько площадок и добавляем зарегистрированные сделки, чтобы была видна разница между запрашиваемой и реальной ценой.",
|
||||
],
|
||||
},
|
||||
...fromFaq("how-accurate"),
|
||||
...fromFaq("vs-marketplace"),
|
||||
{
|
||||
q: "Это подписка? Спишут ли ещё раз?",
|
||||
a: [
|
||||
"Нет. Это разовый платёж за один отчёт — без подписки, автопродлений и скрытых списаний. Отчёт остаётся у вас по ссылке.",
|
||||
],
|
||||
},
|
||||
{
|
||||
q: "Мне начнут звонить после проверки?",
|
||||
a: [
|
||||
"Нет. Для расчёта нужны адрес и параметры квартиры — телефон мы не спрашиваем. Он появляется, только если вы сами решите оставить заявку, и всегда с отдельной галочкой согласия.",
|
||||
],
|
||||
},
|
||||
{
|
||||
q: "Это официальная оценка для банка?",
|
||||
a: [
|
||||
"Нет. Для ипотеки, суда, опеки и нотариуса нужен отчёт аккредитованного оценщика по 135-ФЗ — это отдельная процедура с выездом. МЕРА отвечает на другой вопрос: за сколько квартира реально продаётся на рынке сегодня.",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/** Вопрос v1 по id — `[]`, если id переименовали (тест это ловит). */
|
||||
function fromFaq(id: string): readonly Objection[] {
|
||||
const item = FAQ.find((f) => f.id === id);
|
||||
return item ? [{ q: item.q, a: item.a }] : [];
|
||||
}
|
||||
|
||||
const ITEMS: readonly Objection[] = [
|
||||
...fromFaq("how-do-you-know"),
|
||||
...OBJECTIONS,
|
||||
...fromFaq("personal-data"),
|
||||
...fromFaq("bank-report"),
|
||||
...fromFaq("why-region"),
|
||||
];
|
||||
|
||||
/**
|
||||
* Отсылка к странице про обработку персональных данных внутри ответа
|
||||
* `personal-data`. В `content.ts` она лежит обычным текстом (там нет разметки),
|
||||
* и в старом `Faq.tsx` так текстом и рендерилась — то есть 152-ФЗ-документ
|
||||
* назывался, но открыть его отсюда было нельзя.
|
||||
*
|
||||
* Фраза вынесена константой, а не зашита в разметку: это КОПИЯ куска текста из
|
||||
* `content.ts`, и разъехаться она может молча — переформулируют предложение, и
|
||||
* `indexOf` не найдёт ничего, а ссылка тихо станет обычным текстом. Ровно это
|
||||
* и проверяет `v3-ported-copy.test.ts`: фраза обязана дословно встречаться в
|
||||
* ответе `personal-data`.
|
||||
*/
|
||||
const PRIVACY_PHRASE = "странице про обработку персональных данных";
|
||||
|
||||
/** Абзац ответа: фраза выше отдаётся ссылкой, остальное — как есть. */
|
||||
function renderAnswer(text: string) {
|
||||
const at = text.indexOf(PRIVACY_PHRASE);
|
||||
if (at < 0) return text;
|
||||
return (
|
||||
<>
|
||||
{text.slice(0, at)}
|
||||
<PublicLink to="privacy" className={styles.objAnswerLink}>
|
||||
{PRIVACY_PHRASE}
|
||||
</PublicLink>
|
||||
{text.slice(at + PRIVACY_PHRASE.length)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ObjectionsV3() {
|
||||
return (
|
||||
<section className={styles.objSection} aria-labelledby="obj-v3-title">
|
||||
|
|
@ -110,7 +147,7 @@ export function ObjectionsV3() {
|
|||
</summary>
|
||||
<div className={styles.objAnswer}>
|
||||
{o.a.map((p) => (
|
||||
<p key={p.slice(0, 24)}>{p}</p>
|
||||
<p key={p.slice(0, 24)}>{renderAnswer(p)}</p>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
|
|
|
|||
|
|
@ -2127,6 +2127,20 @@
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Ссылка ВНУТРИ предложения (отсылка к странице про обработку персональных
|
||||
данных в ответе `personal-data`) — то же inline-исключение из правила зоны
|
||||
нажатия, что у .ftrInlineLink: раздувать строку текста было бы хуже. */
|
||||
.objAnswerLink {
|
||||
color: var(--b2c-accent-text);
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.objAnswerLink:hover {
|
||||
color: var(--b2c-ink);
|
||||
}
|
||||
|
||||
/* --- Статьи --------------------------------------------------------------- */
|
||||
.artSection {
|
||||
max-width: 1280px;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue