feat(mera/b2c): раздел статей и хаб документов (макет 29.08) + числа статьи заменены на замер #3223
15 changed files with 1190 additions and 71 deletions
|
|
@ -351,7 +351,7 @@ meraocenka.ru {
|
|||
# страница: владелец сравнивает его с текущим лэндингом. Оно `noindex` и
|
||||
# ни с одной страницы на него нет ссылки. Убрать эту строку в тот момент,
|
||||
# когда вариант выберут и он станет корнем.
|
||||
@meraPages path /estimate /oferta /refund /privacy /v3
|
||||
@meraPages path /estimate /oferta /refund /privacy /v3 /articles /articles/kak-ocenit-kvartiru /docs
|
||||
handle @meraPages {
|
||||
rewrite * /trade-in/mera-public{path}
|
||||
reverse_proxy tradein-frontend:3000 {
|
||||
|
|
@ -365,7 +365,7 @@ meraocenka.ru {
|
|||
# голый 404 (так было и до этого PR, с момента #2615). Заодно это
|
||||
# замыкает цепочку для длинных адресов со слэшем: они приходят на короткий
|
||||
# со слэшем и здесь нормализуются.
|
||||
@meraShortSlash path_regexp shortslash ^/(estimate|oferta|refund|privacy|v3)/$
|
||||
@meraShortSlash path_regexp shortslash ^/(estimate|oferta|refund|privacy|v3|articles|articles/kak-ocenit-kvartiru|docs)/$
|
||||
handle @meraShortSlash {
|
||||
redir * /{re.shortslash.1} permanent
|
||||
}
|
||||
|
|
@ -413,7 +413,7 @@ meraocenka.ru {
|
|||
# порядок директив внутри `handle` определяет Caddy, и без неё `redir`
|
||||
# выполняется РАНЬШЕ `uri`, отдавая Location, равный исходному адресу
|
||||
# (бесконечный цикл; поймано на локальном стенде).
|
||||
@meraLongPages path /trade-in/mera-public/estimate /trade-in/mera-public/estimate/ /trade-in/mera-public/oferta /trade-in/mera-public/oferta/ /trade-in/mera-public/refund /trade-in/mera-public/refund/ /trade-in/mera-public/privacy /trade-in/mera-public/privacy/ /trade-in/mera-public/v3 /trade-in/mera-public/v3/
|
||||
@meraLongPages path /trade-in/mera-public/estimate /trade-in/mera-public/estimate/ /trade-in/mera-public/oferta /trade-in/mera-public/oferta/ /trade-in/mera-public/refund /trade-in/mera-public/refund/ /trade-in/mera-public/privacy /trade-in/mera-public/privacy/ /trade-in/mera-public/v3 /trade-in/mera-public/v3/ /trade-in/mera-public/articles /trade-in/mera-public/articles/ /trade-in/mera-public/articles/kak-ocenit-kvartiru /trade-in/mera-public/articles/kak-ocenit-kvartiru/ /trade-in/mera-public/docs /trade-in/mera-public/docs/
|
||||
handle @meraLongPages {
|
||||
route {
|
||||
uri strip_prefix /trade-in/mera-public
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@ const V3_SOURCES = [
|
|||
// Макет 29.08: лента и игра тоже стоят на реквизитных величинах.
|
||||
"_components/v3/DealsTickerV3.tsx",
|
||||
"_components/v3/GuessGameV3.tsx",
|
||||
// Раздел статей (29.08) — плитки §4 статьи берут те же плейсхолдеры. Без
|
||||
// этой строки гейт остался бы зелёным при снятии noindex: он сканировал бы
|
||||
// только лэндинг, а величины уже живут и на статье.
|
||||
"articles/articles-content.ts",
|
||||
];
|
||||
|
||||
const PLACEHOLDER_EXPORTS = [
|
||||
|
|
|
|||
|
|
@ -147,6 +147,32 @@ describe("короткие адреса публичного домена", () =
|
|||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* ТРЕТИЙ СПИСОК, до 29.08.2026 не проверявшийся ничем. `@meraPages path`
|
||||
* матчит адрес БЕЗ завершающего слэша, поэтому `/docs/` мимо него проходит и
|
||||
* улетает в catch-all 404. Нормализует такие адреса отдельный матчер
|
||||
* `@meraShortSlash`, и его альтернация — ещё одно место, где список страниц
|
||||
* можно молча не дополнить. Ровно тот же класс дефекта, ради которого
|
||||
* заведены две проверки выше; здесь он до сих пор был не закрыт.
|
||||
*/
|
||||
it("каждый короткий адрес нормализуется и со слэшем на конце", () => {
|
||||
const slashLine =
|
||||
meraBlock.match(/@meraShortSlash path_regexp \w+ \^\/\(([^)]+)\)/)?.[1] ??
|
||||
"";
|
||||
const alternatives = slashLine.split("|").filter(Boolean);
|
||||
expect(
|
||||
alternatives.length,
|
||||
"не найдена альтернация @meraShortSlash — зонд сломан, проверка ниже пройдёт вхолостую",
|
||||
).toBeGreaterThan(0);
|
||||
for (const route of [...Object.values(PUBLIC_ROUTES), ...PREVIEW_ONLY]) {
|
||||
if (route === "/") continue; // корень слэшем и так заканчивается
|
||||
expect(
|
||||
alternatives.includes(route.slice(1)),
|
||||
`${route}/ (со слэшем) не нормализуется — адрес отдаст 404 вместо 301`,
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("basePath не протекает в публичные ссылки", () => {
|
||||
|
|
|
|||
|
|
@ -1,21 +1,22 @@
|
|||
import { B2B_URL } from "../../content";
|
||||
import styles from "../../landing-v3.module.css";
|
||||
import { PublicLink } from "../PublicLink";
|
||||
import { B2B_URL } from "../content";
|
||||
import styles from "../landing-v3.module.css";
|
||||
import { PublicLink } from "./PublicLink";
|
||||
|
||||
/**
|
||||
* Шапка страницы проверки — намеренно короче лэндинговой (`v3/HeaderV3.tsx`).
|
||||
* Короткая шапка внутренних страниц — проверка (`estimate/`), статьи
|
||||
* (`articles/`), документы (`docs/`). Намеренно короче лэндинговой
|
||||
* (`v3/HeaderV3.tsx`).
|
||||
*
|
||||
* Из неё убраны навигация по секциям лэндинга, селектор города и CTA
|
||||
* «Проверить за 30 секунд». Причина у всех трёх одна: человек уже на экране
|
||||
* проверки. Якоря вроде `#accuracy` тут просто не на что навести (секций нет
|
||||
* на этой странице), город выбирается прямо в форме — а второй селектор
|
||||
* города в шапке означал бы два места, задающих одно значение, — а CTA вёл бы
|
||||
* на страницу, где посетитель уже стоит.
|
||||
* «Проверить за 30 секунд». Причина у всех трёх одна: человек уже ушёл с
|
||||
* лэндинга. Якоря вроде `#accuracy` тут просто не на что навести (секций нет
|
||||
* на этих страницах), город выбирается прямо в форме проверки — а второй
|
||||
* селектор города в шапке означал бы два места, задающих одно значение.
|
||||
*
|
||||
* Логотип отсюда — ссылка на главную: это единственный обратный путь на
|
||||
* лэндинг, и он же то, что человек инстинктивно нажимает.
|
||||
*/
|
||||
export function EstimateHeader() {
|
||||
export function InnerHeader() {
|
||||
return (
|
||||
<header className={styles.hdrRoot}>
|
||||
<div className={styles.hdrInner}>
|
||||
|
|
@ -1,34 +1,13 @@
|
|||
/**
|
||||
* ArticlesV3 — «Разборы на данных» (макет 29.08, секция «V3 — статьи»).
|
||||
*
|
||||
* Раздела статей в продукте НЕ СУЩЕСТВУЕТ (страницы «МЕРА статьи»/«МЕРА
|
||||
* статья» из макета — следующий шард). Поэтому карточки рендерятся <article>,
|
||||
* а НЕ <a>: ссылка «в никуда» на превью читалась бы как сломанная навигация.
|
||||
* Ссылка «Все статьи →» по той же причине не рендерится вовсе — появится
|
||||
* вместе с разделом. Темы и рубрики карточек — из макета дословно.
|
||||
* Раздел статей существует с 29.08: живая статья одна — её карточка ведёт на
|
||||
* страницу; остальные две честно помечены «СКОРО» и ссылками не являются.
|
||||
*/
|
||||
|
||||
import { PublicLink } from "../PublicLink";
|
||||
import styles from "../../landing-v3.module.css";
|
||||
|
||||
interface ArticleCard {
|
||||
readonly rubric: string;
|
||||
readonly title: string;
|
||||
}
|
||||
|
||||
const ARTICLES: readonly ArticleCard[] = [
|
||||
{
|
||||
rubric: "ЦЕНООБРАЗОВАНИЕ · 6 МИН",
|
||||
title: "Как оценить квартиру для продажи — без риелтора и «на глазок»",
|
||||
},
|
||||
{
|
||||
rubric: "СРОКИ · 5 МИН",
|
||||
title: "Сколько продаётся квартира в 2026 году и что «подвешивает» объявление",
|
||||
},
|
||||
{
|
||||
rubric: "ТОРГ · 7 МИН",
|
||||
title: "Завысить и торговаться или поставить в рынок: что выгоднее",
|
||||
},
|
||||
];
|
||||
import { ARTICLE_CARDS } from "../../articles/articles-content";
|
||||
|
||||
export function ArticlesV3() {
|
||||
return (
|
||||
|
|
@ -37,14 +16,32 @@ export function ArticlesV3() {
|
|||
<h2 id="articles-v3-title" className={styles.artTitle}>
|
||||
Разборы на данных
|
||||
</h2>
|
||||
<PublicLink to="articles" className={styles.artAllLink}>
|
||||
Все статьи →
|
||||
</PublicLink>
|
||||
</div>
|
||||
<div className={styles.artGrid}>
|
||||
{ARTICLES.map((a) => (
|
||||
<article key={a.title} className={styles.artCard}>
|
||||
<div className={styles.artRubric}>{a.rubric}</div>
|
||||
<div className={styles.artCardTitle}>{a.title}</div>
|
||||
</article>
|
||||
))}
|
||||
{ARTICLE_CARDS.map((a) =>
|
||||
a.route ? (
|
||||
<PublicLink
|
||||
key={a.title}
|
||||
to={a.route}
|
||||
className={styles.artCardLink}
|
||||
>
|
||||
<div className={styles.artRubric}>{a.rubric}</div>
|
||||
<div className={styles.artCardTitle}>{a.title}</div>
|
||||
<div className={styles.artCardLead}>{a.lead}</div>
|
||||
<div className={styles.artReadMore}>Читать →</div>
|
||||
</PublicLink>
|
||||
) : (
|
||||
<article key={a.title} className={styles.artCard}>
|
||||
<div className={styles.artRubric}>{a.rubric}</div>
|
||||
<div className={styles.artCardTitle}>{a.title}</div>
|
||||
<div className={styles.artCardLead}>{a.lead}</div>
|
||||
<div className={styles.artSoon}>СКОРО</div>
|
||||
</article>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -14,8 +14,11 @@
|
|||
* приём платежей не подключён (см. TODO у нижней строки) — заявлять способы
|
||||
* оплаты, которых нет, подпадает под ст. 5 ФЗ «О рекламе».
|
||||
*
|
||||
* «Статьи» в «Разделах» неактивны тем же образом, что в шапке (HeaderV3):
|
||||
* раздела нет в коде.
|
||||
* «Статьи» в «Разделах» стали ссылкой 29.08.2026 — раздел появился
|
||||
* (`articles/`). До этого пункт был неактивен тем же образом, что в шапке
|
||||
* (HeaderV3). «Документы и условия» — хаб `docs/`: он НЕ заменяет прямые
|
||||
* ссылки на оферту/возврат/политику (они обязаны быть в подвале и остаются),
|
||||
* а добавляет одну точку входа с тарифами и порядком возврата.
|
||||
*
|
||||
* ССЫЛКИ — КОРОТКИЕ (15.08.2026). «Проверить квартиру» ведёт на `/estimate` —
|
||||
* отдельный экран проверки, а не на якорь формы в герое. «МЕРА для бизнеса»
|
||||
|
|
@ -64,13 +67,9 @@ export function FooterV3() {
|
|||
<PublicLink to="estimate" className={styles.ftrLink}>
|
||||
Проверить квартиру
|
||||
</PublicLink>
|
||||
<span
|
||||
className={styles.ftrLinkDisabled}
|
||||
aria-disabled="true"
|
||||
title="Раздел скоро откроется"
|
||||
>
|
||||
<PublicLink to="articles" className={styles.ftrLink}>
|
||||
Статьи
|
||||
</span>
|
||||
</PublicLink>
|
||||
<a href={B2B_URL} className={styles.ftrLink}>
|
||||
МЕРА для бизнеса
|
||||
</a>
|
||||
|
|
@ -115,6 +114,9 @@ export function FooterV3() {
|
|||
<PublicLink to="privacy" className={styles.ftrLink}>
|
||||
Обработка персональных данных
|
||||
</PublicLink>
|
||||
<PublicLink to="docs" className={styles.ftrLink}>
|
||||
Документы и условия
|
||||
</PublicLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,16 +5,17 @@
|
|||
* Логотип НЕ ссылка: на публичном домене корень — это сам лэндинг, то есть
|
||||
* ссылка вела бы на текущую же страницу.
|
||||
*
|
||||
* НЕАКТИВНЫЕ ПУНКТЫ. «Статьи», «Проверьте себя» и «Продажа под ключ» —
|
||||
* разделов нет в коде: ни маршрута, ни секции. Раньше три якоря вели на
|
||||
* `/mera-public#game` и `#turnkey`, которых не существует НИГДЕ, — то есть
|
||||
* клик уводил на другую страницу и там ничего не находил. Пункты приведены к
|
||||
* тому же виду, что «Статьи»: видны, но не кликабельны, пока раздела нет.
|
||||
* НЕАКТИВНЫЕ ПУНКТЫ (29.08.2026). Осталась одна «Продажа под ключ» —
|
||||
* партнёрской формы нет ни в коде, ни у владельца, и пункт остаётся видимым,
|
||||
* но некликабельным. «Проверьте себя» и «Статьи» раньше стояли рядом с ней по
|
||||
* той же причине; теперь секция игры (`#game`, GuessGameV3) и раздел статей
|
||||
* (`/articles`) существуют, поэтому оба пункта снова ведут по назначению.
|
||||
* Правило прежнее: пункт становится ссылкой ТОЛЬКО когда цель существует.
|
||||
*
|
||||
* ЯКОРЯ — голые (`#accuracy`, `#check`), потому что эта шапка рендерится
|
||||
* только на странице, где эти секции и лежат. На юридических страницах стоит
|
||||
* другая шапка (`_components/SiteHeader.tsx`), на `/estimate` — своя,
|
||||
* укороченная (`_components/estimate/EstimateHeader.tsx`): туда переносить
|
||||
* укороченная (`_components/InnerHeader.tsx`): туда переносить
|
||||
* навигацию по секциям лэндинга незачем, там человек уже пришёл считать.
|
||||
*
|
||||
* «Для бизнеса» ведёт на АБСОЛЮТНЫЙ адрес B2B-контура: относительный «/» на
|
||||
|
|
@ -26,6 +27,7 @@
|
|||
|
||||
import { B2B_URL } from "../../content";
|
||||
import styles from "../../landing-v3.module.css";
|
||||
import { PublicLink } from "../PublicLink";
|
||||
|
||||
import { CityPicker } from "./CityPicker";
|
||||
|
||||
|
|
@ -47,16 +49,19 @@ export function HeaderV3() {
|
|||
<a href="#accuracy" className={styles.hdrNavLink}>
|
||||
Точность
|
||||
</a>
|
||||
{["Проверьте себя", "Продажа под ключ", "Статьи"].map((name) => (
|
||||
<span
|
||||
key={name}
|
||||
className={styles.hdrNavDisabled}
|
||||
aria-disabled="true"
|
||||
title="Раздел скоро откроется"
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
))}
|
||||
<a href="#game" className={styles.hdrNavLink}>
|
||||
Проверьте себя
|
||||
</a>
|
||||
<span
|
||||
className={styles.hdrNavDisabled}
|
||||
aria-disabled="true"
|
||||
title="Раздел скоро откроется"
|
||||
>
|
||||
Продажа под ключ
|
||||
</span>
|
||||
<PublicLink to="articles" className={styles.hdrNavLink}>
|
||||
Статьи
|
||||
</PublicLink>
|
||||
</nav>
|
||||
|
||||
<div className={styles.hdrRight}>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,181 @@
|
|||
/**
|
||||
* Контент раздела «Статьи» — макет 29.08 («МЕРА статья.dc.html», текст Макса).
|
||||
*
|
||||
* ЧИСЛА ИЗ МАКЕТА СНЯТЫ (29.08.2026). Макет утверждал «по нашим данным,
|
||||
* средний разрыв между первой ценой в объявлении и ценой сделки — 4,1%, а у
|
||||
* провисевших дольше трёх месяцев доходит до 9%», «расхождение с кадастровой
|
||||
* до 40%» и плитки −4,3% / ×2,4 / 83%. Прод-замер (poincare, 29.08.2026)
|
||||
* не подтвердил ни одно:
|
||||
*
|
||||
* | макет | что показал замер |
|
||||
* | разрыв объявление↔сделка | ИЗМЕРИТЬ НЕЛЬЗЯ: связки листинга со сделкой |
|
||||
* | | в данных нет (cadastral_number пуст у всех |
|
||||
* | | 108 623 сделок и 111 693 объявлений) |
|
||||
* | «до 9%» у долгостоя | 62% снижали, медиана −4,9% (n=2513) |
|
||||
* | кадастровая «до 40%» | ПРОВЕРИТЬ НЕЧЕМ: кадастровой стоимости |
|
||||
* | | ПОМЕЩЕНИЙ нет ни в одной базе |
|
||||
* | −4,3% за месяц простоя | медиана внутрилистинговой просадки −1,1%/мес |
|
||||
* | ×2,4 дольше продаётся | 79 дн против 76 → ×1,04 |
|
||||
* | 83% в ценовой коридор | коридор шириной 47% от цены — не точность |
|
||||
*
|
||||
* Поэтому §1 и §4 стоят на СВОЁМ замере (см. поля `source` — там основание и
|
||||
* его границы), кадастровое число снято, а поправки §3 честно помечены как
|
||||
* рыночная практика, а не наш замер. Плейсхолдеров marketing-v3 в статье
|
||||
* больше нет — но строка `articles/articles-content.ts` в `V3_SOURCES` гейта
|
||||
* #2904 оставлена намеренно: вернётся плейсхолдер — гейт его увидит.
|
||||
*
|
||||
* ЗАМЕР, НА КОТОРОМ СТОИТ ТЕКСТ. 6202 объявления квартир Домклик в
|
||||
* Екатеринбурге, срез 18.07.2026, парно «цена публикации → цена на дату
|
||||
* среза». Две границы названы прямо в тексте, потому что без них число врёт:
|
||||
* (1) это движение ЗАПРАШИВАЕМОЙ цены, а не скидка на сделке; (2) объявлений
|
||||
* моложе трёх недель в выборке нет вовсе (прогон обогащения 18.07 их не
|
||||
* захватил), а они снижают цену реже всех — поэтому доля по всему живому
|
||||
* рынку ниже примерно на 3 п.п. Цифра по долгостою от этой дыры не зависит.
|
||||
* Дерево mera-public закрыто noindex (layout.tsx).
|
||||
*/
|
||||
|
||||
import { SERVICE_PRICE_RUB } from "../content";
|
||||
|
||||
export interface ArticleCardMeta {
|
||||
/**
|
||||
* Ключ маршрута в `PUBLIC_ROUTES` — или `null`, если статьи ещё нет.
|
||||
* Именно ключ, а не слаг: карточка сама знает, куда ведёт, и добавление
|
||||
* второй статьи не может увести её на первую (так и было, пока
|
||||
* `to="articleHowTo"` стоял константой в JSX).
|
||||
*/
|
||||
readonly route: "articleHowTo" | null;
|
||||
readonly rubric: string;
|
||||
readonly title: string;
|
||||
/** Подводка карточки — дословно из макета. */
|
||||
readonly lead: string;
|
||||
}
|
||||
|
||||
/** Карточки списка. slug=null — статьи ещё нет (карточка без ссылки). */
|
||||
export const ARTICLE_CARDS: readonly ArticleCardMeta[] = [
|
||||
{
|
||||
route: "articleHowTo",
|
||||
rubric: "ЦЕНООБРАЗОВАНИЕ · 6 МИН",
|
||||
title: "Как оценить квартиру для продажи — без риелтора и «на глазок»",
|
||||
lead: "Почему цена соседа — не ориентир, и как считать по аналогам.",
|
||||
},
|
||||
{
|
||||
route: null,
|
||||
rubric: "СРОКИ · 5 МИН",
|
||||
title: "Сколько продаётся квартира в 2026 году и что «подвешивает» объявление",
|
||||
lead: "Средние сроки по типам квартир и главные причины простоя.",
|
||||
},
|
||||
{
|
||||
route: null,
|
||||
rubric: "ТОРГ · 7 МИН",
|
||||
title: "Завысить и торговаться или поставить в рынок: что выгоднее",
|
||||
lead: "Считаем на данных, сколько теряет «висящая» квартира каждый месяц.",
|
||||
},
|
||||
];
|
||||
|
||||
export const HOWTO = {
|
||||
rubric: "ЦЕНООБРАЗОВАНИЕ · 6 МИН ЧТЕНИЯ",
|
||||
title: "Как оценить квартиру для продажи — без риелтора и «на глазок»",
|
||||
lead: [
|
||||
"Большинство продавцов назначают цену одним из трёх способов: смотрят объявление соседа, берут кадастровую стоимость или спрашивают знакомого риелтора. Все три дают ошибку в обе стороны — и обе стоят денег. Завышенная цена держит квартиру в продаже месяцами, заниженная уносит сотни тысяч сразу.",
|
||||
"Правильный способ один: сравнение с реальными аналогами. Ниже — как сделать это самому за вечер.",
|
||||
],
|
||||
/**
|
||||
* Оглавление. `id` — тот же якорь, что стоит у заголовка секции ниже:
|
||||
* страница рендерит `id` ИЗ ЭТОГО списка, а не из захардкоженных «s1…s5»,
|
||||
* иначе перестановка пункта молча увела бы ссылку не туда.
|
||||
*/
|
||||
toc: [
|
||||
{ id: "s1", title: "Почему цена соседа — плохой ориентир" },
|
||||
{ id: "s2", title: "Кто такие «аналоги» и как их отобрать" },
|
||||
{ id: "s3", title: "Считаем цену за метр" },
|
||||
{ id: "s4", title: "Проверяем цену сроком продажи" },
|
||||
{ id: "s5", title: "Пять ошибок, которые стоят дороже всего" },
|
||||
],
|
||||
s1: {
|
||||
id: "s1",
|
||||
heading: "1. Почему цена соседа — плохой ориентир",
|
||||
body: "В объявлении вы видите цену предложения, а не сделки. Между ними почти всегда есть разрыв: продавец заложил торг, а покупатель этот торг забрал. Насколько именно — не знает никто: цены сделок Росреестр публикует без привязки к объявлению, и сопоставить одно с другим нельзя. Зато отлично видно, как продавец сам двигает цену, пока квартира не продаётся. По объявлениям Екатеринбурга, провисевшим дольше трёх месяцев, цену снижали 62% продавцов — в медиане на 4,9% от той, с которой они начинали. В первый месяц продажи снижают только 7%.",
|
||||
source:
|
||||
"ЗАМЕР 18.07.2026 · 6202 ОБЪЯВЛЕНИЯ ДОМКЛИК В ЕКАТЕРИНБУРГЕ, ПРОВИСЕВШИЕ НЕ МЕНЬШЕ ТРЁХ НЕДЕЛЬ · ЭТО ДВИЖЕНИЕ ЦЕНЫ В ОБЪЯВЛЕНИИ, А НЕ СКИДКА НА СДЕЛКЕ",
|
||||
memo: "Ориентироваться нужно на цены сделок, а не на цены объявлений. Объявление говорит, чего хочет продавец; сделка — что готов заплатить рынок.",
|
||||
},
|
||||
s2: {
|
||||
id: "s2",
|
||||
heading: "2. Кто такие «аналоги» и как их отобрать",
|
||||
body: "Аналог — это квартира, за которую тот же покупатель рассматривал бы вашу. Не «такая же по площади», а взаимозаменяемая по совокупности параметров. Рабочий фильтр выглядит так:",
|
||||
filter: [
|
||||
{ k: "РАССТОЯНИЕ", v: "до 1 км от вашего дома — дальше начинается другой рынок" },
|
||||
{ k: "ПЛОЩАДЬ", v: "±15% от вашей — «двушка 54 м²» и «двушка 68 м²» это разные товары" },
|
||||
{ k: "КОМНАТЫ", v: "ровно столько же; студия и однушка не взаимозаменяемы" },
|
||||
{ k: "ТИП ДОМА", v: "панель к панели, монолит к монолиту, год постройки ±10 лет" },
|
||||
{ k: "ЭТАЖ", v: "не первый и не последний в отдельную группу — по ним скидка своя" },
|
||||
],
|
||||
after:
|
||||
"Если под фильтр попадает меньше пяти квартир — расширяйте радиус, а не критерии. Сравнивать «двушку» с «трёшкой» бессмысленно, а соседний квартал обычно живёт по той же цене. МЕРА поступает так же: сначала перестаёт требовать близкий год постройки, потом расширяет радиус до 2 км, потом допускает площадь ±25% и соседнюю комнатность — и лишь в крайнем случае уходит на 3 и 5 км.",
|
||||
footnote:
|
||||
"ЭТО ТЕ ЖЕ ПОРОГИ, ЧТО СТОЯТ В ОЦЕНЩИКЕ МЕРЫ: РАДИУС 1 КМ, ПЛОЩАДЬ ±15%, МИНИМУМ ПЯТЬ АНАЛОГОВ",
|
||||
},
|
||||
s3: {
|
||||
id: "s3",
|
||||
heading: "3. Считаем цену за метр и приводим к своей квартире",
|
||||
body: "Возьмите отобранные аналоги, посчитайте по каждому цену за квадратный метр и отбросьте два крайних значения — сверху и снизу. Медиана оставшихся и есть база вашей цены. Дальше добавьте поправки.",
|
||||
factors: [
|
||||
{ name: "Первый или последний этаж", value: "−5…−8%" },
|
||||
{ name: "Требуется косметический ремонт", value: "−4…−7%" },
|
||||
{ name: "Окна во двор, не на трассу", value: "+2…+4%" },
|
||||
{ name: "Свежий ремонт «под ключ»", value: "+3…+6%" },
|
||||
{ name: "Пешая доступность метро (до 10 мин)", value: "+5…+9%" },
|
||||
{ name: "Свободная продажа, без ипотечных обременений", value: "+1…+3%" },
|
||||
],
|
||||
footnote:
|
||||
"ОРИЕНТИРЫ РЫНОЧНОЙ ПРАКТИКИ, А НЕ ЗАМЕР МЕРЫ · В КОНКРЕТНОМ ДОМЕ ПОПРАВКА ОТЛИЧАЕТСЯ",
|
||||
},
|
||||
s4: {
|
||||
id: "s4",
|
||||
heading: "4. Проверяем цену сроком продажи",
|
||||
body: "Цена без срока — половина ответа. Одна и та же квартира уйдёт за 30 дней по одной цене и за 90 — по другой. Проверить свою цену сроком можно и без нас: посмотрите, как часто продавцы вокруг сдаются и снижают её. Чем дольше объявление висит, тем чаще это происходит.",
|
||||
tiles: [
|
||||
{ value: "7%", label: "снижают цену в первый месяц продажи" },
|
||||
{ value: "38%", label: "снижают на втором и третьем месяце" },
|
||||
{
|
||||
value: "62%",
|
||||
label: "снижают, если квартира висит дольше трёх месяцев",
|
||||
},
|
||||
],
|
||||
source:
|
||||
"ТОТ ЖЕ ЗАМЕР · САМЫХ СВЕЖИХ ОБЪЯВЛЕНИЙ (МОЛОЖЕ ТРЁХ НЕДЕЛЬ) В ВЫБОРКЕ НЕТ, А ОНИ СНИЖАЮТ РЕЖЕ ВСЕХ — ПО ВСЕМУ ЖИВОМУ РЫНКУ ДОЛЯ БЫЛА БЫ НИЖЕ",
|
||||
},
|
||||
cta: {
|
||||
eyebrow: "ЕСЛИ СЧИТАТЬ ВРУЧНУЮ НЕКОГДА",
|
||||
title: "МЕРА делает всё это за 30 секунд",
|
||||
body: `Мы находим аналоги, считаем медиану, применяем поправки и проверяем цену сроком продажи. Бесплатно покажем рынок вокруг, за ${SERVICE_PRICE_RUB} ₽ — вашу цену и прогноз.`,
|
||||
},
|
||||
s5: {
|
||||
id: "s5",
|
||||
heading: "5. Пять ошибок, которые стоят дороже всего",
|
||||
mistakes: [
|
||||
{
|
||||
title: "Закладывать «запас на торг» в 10%",
|
||||
body: "Покупатель не увидит вашу квартиру в фильтре по цене — торговаться будет некому.",
|
||||
},
|
||||
{
|
||||
title: "Считать по кадастровой стоимости",
|
||||
body: "Она нужна для налога, а не для рынка: её считают массовой моделью по площади, типу и году дома, не заходя в квартиру. С ценой конкретного объекта она связана слабо — и в обе стороны.",
|
||||
},
|
||||
{
|
||||
title: "Добавлять к цене стоимость ремонта",
|
||||
body: "Рынок оплачивает состояние, а не ваши чеки. Ремонт возвращается частично и не всегда.",
|
||||
},
|
||||
{
|
||||
title: "Сравнивать с квартирой, которая уже полгода висит",
|
||||
body: "Это не ориентир, а пример того, как делать не надо.",
|
||||
},
|
||||
{
|
||||
title: "Не пересматривать цену через месяц",
|
||||
body: "Если за 30 дней нет ни одного показа — цена выше рынка, и рынок вам об этом уже сказал.",
|
||||
},
|
||||
],
|
||||
},
|
||||
summary:
|
||||
"Смотрите на цены сделок, а не объявлений. Берите 5–14 действительно взаимозаменяемых аналогов, считайте медиану цены за метр, применяйте поправки и обязательно проверяйте цену сроком продажи. Если считать некогда — это ровно то, что МЕРА делает автоматически.",
|
||||
} as const;
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
import type { Metadata } from "next";
|
||||
|
||||
import { InnerHeader } from "../../_components/InnerHeader";
|
||||
import { PublicLink } from "../../_components/PublicLink";
|
||||
import { FooterV3 } from "../../_components/v3/FooterV3";
|
||||
import styles from "../../landing-v3.module.css";
|
||||
import { ARTICLE_CARDS, HOWTO } from "../articles-content";
|
||||
|
||||
/**
|
||||
* Статья «Как оценить квартиру для продажи» — макет 29.08 («МЕРА статья»),
|
||||
* текст из макета целиком. Числа — реквизит макета: см. шапку
|
||||
* articles-content.ts; страница, как и всё дерево, под noindex из layout.
|
||||
*
|
||||
* Серверный компонент без клиентского JS: содержание — якорные ссылки,
|
||||
* секции — обычные заголовки с id.
|
||||
*/
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `${HOWTO.title} — МЕРА`,
|
||||
robots: { index: false, follow: false },
|
||||
};
|
||||
|
||||
export default function HowToArticlePage() {
|
||||
return (
|
||||
<>
|
||||
<InnerHeader />
|
||||
<main id="content" className={styles.artPage} tabIndex={-1}>
|
||||
<nav className={styles.artCrumbs} aria-label="Хлебные крошки">
|
||||
<PublicLink to="home" className={styles.artCrumbLink}>
|
||||
Главная
|
||||
</PublicLink>
|
||||
<span aria-hidden="true">/</span>
|
||||
<PublicLink to="articles" className={styles.artCrumbLink}>
|
||||
Статьи
|
||||
</PublicLink>
|
||||
</nav>
|
||||
|
||||
<header className={styles.artPageHead}>
|
||||
<div className={styles.repEyebrow}>{HOWTO.rubric}</div>
|
||||
<h1 className={styles.artPageTitle}>{HOWTO.title}</h1>
|
||||
</header>
|
||||
|
||||
<nav className={styles.artToc} aria-label="Содержание">
|
||||
<div className={styles.artTocLabel}>СОДЕРЖАНИЕ</div>
|
||||
<ol className={styles.artTocList}>
|
||||
{HOWTO.toc.map((t) => (
|
||||
<li key={t.id}>
|
||||
<a href={`#${t.id}`}>{t.title}</a>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div className={styles.artBody}>
|
||||
{HOWTO.lead.map((p) => (
|
||||
<p key={p.slice(0, 24)} className={styles.artP}>
|
||||
{p}
|
||||
</p>
|
||||
))}
|
||||
|
||||
<h2 id={HOWTO.s1.id} className={styles.artH2}>
|
||||
{HOWTO.s1.heading}
|
||||
</h2>
|
||||
<p className={styles.artP}>{HOWTO.s1.body}</p>
|
||||
<div className={styles.artFootnote}>{HOWTO.s1.source}</div>
|
||||
<aside className={styles.artMemo}>
|
||||
<div className={styles.artMemoLabel}>ЗАПОМНИТЬ</div>
|
||||
<p>{HOWTO.s1.memo}</p>
|
||||
</aside>
|
||||
|
||||
<h2 id={HOWTO.s2.id} className={styles.artH2}>
|
||||
{HOWTO.s2.heading}
|
||||
</h2>
|
||||
<p className={styles.artP}>{HOWTO.s2.body}</p>
|
||||
<div className={styles.artKv}>
|
||||
{HOWTO.s2.filter.map((row) => (
|
||||
<div key={row.k} className={styles.artKvRow}>
|
||||
<span className={styles.artKvKey}>{row.k}</span>
|
||||
<span className={styles.artKvVal}>{row.v}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.artFootnote}>{HOWTO.s2.footnote}</div>
|
||||
<p className={styles.artP}>{HOWTO.s2.after}</p>
|
||||
|
||||
<h2 id={HOWTO.s3.id} className={styles.artH2}>
|
||||
{HOWTO.s3.heading}
|
||||
</h2>
|
||||
<p className={styles.artP}>{HOWTO.s3.body}</p>
|
||||
<div className={styles.artTable} role="table">
|
||||
<div className={styles.artTableHead} role="row">
|
||||
<span role="columnheader">ФАКТОР</span>
|
||||
<span role="columnheader" className={styles.artTableRight}>
|
||||
ПОПРАВКА К ЦЕНЕ
|
||||
</span>
|
||||
</div>
|
||||
{HOWTO.s3.factors.map((f) => (
|
||||
<div key={f.name} className={styles.artTableRow} role="row">
|
||||
<span role="cell">{f.name}</span>
|
||||
<span role="cell" className={styles.artTableRight}>
|
||||
{f.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.artFootnote}>{HOWTO.s3.footnote}</div>
|
||||
|
||||
<h2 id={HOWTO.s4.id} className={styles.artH2}>
|
||||
{HOWTO.s4.heading}
|
||||
</h2>
|
||||
<p className={styles.artP}>{HOWTO.s4.body}</p>
|
||||
<div className={styles.artTiles}>
|
||||
{HOWTO.s4.tiles.map((t) => (
|
||||
<div key={t.label} className={styles.artTile}>
|
||||
<div className={styles.artTileValue}>{t.value}</div>
|
||||
<div className={styles.artTileLabel}>{t.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.artFootnote}>{HOWTO.s4.source}</div>
|
||||
|
||||
<div className={styles.artCta}>
|
||||
<div className={styles.artCtaEyebrow}>{HOWTO.cta.eyebrow}</div>
|
||||
<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>
|
||||
|
||||
<h2 id={HOWTO.s5.id} className={styles.artH2}>
|
||||
{HOWTO.s5.heading}
|
||||
</h2>
|
||||
<ol className={styles.artMistakes}>
|
||||
{HOWTO.s5.mistakes.map((m, i) => (
|
||||
<li key={m.title} className={styles.artMistake}>
|
||||
<span className={styles.artMistakeNum}>
|
||||
{String(i + 1).padStart(2, "0")}
|
||||
</span>
|
||||
<div>
|
||||
<div className={styles.artMistakeTitle}>{m.title}</div>
|
||||
<div className={styles.artMistakeBody}>{m.body}</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
<div className={styles.artSummary}>
|
||||
<div className={styles.artTocLabel}>КОРОТКО</div>
|
||||
<p>{HOWTO.summary}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className={styles.artMore} aria-label="Читайте дальше">
|
||||
<div className={styles.artHead}>
|
||||
<h2 className={styles.artTitle}>Читайте дальше</h2>
|
||||
<PublicLink to="articles" className={styles.artAllLink}>
|
||||
Все статьи →
|
||||
</PublicLink>
|
||||
</div>
|
||||
<div className={styles.artGrid}>
|
||||
{ARTICLE_CARDS.filter((c) => c.route !== "articleHowTo").map(
|
||||
(c) => (
|
||||
<article key={c.title} className={styles.artCard}>
|
||||
<div className={styles.artRubric}>{c.rubric}</div>
|
||||
<div className={styles.artCardTitle}>{c.title}</div>
|
||||
<div className={styles.artCardLead}>{c.lead}</div>
|
||||
<div className={styles.artSoon}>СКОРО</div>
|
||||
</article>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<FooterV3 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
98
tradein-mvp/frontend/src/app/mera-public/articles/page.tsx
Normal file
98
tradein-mvp/frontend/src/app/mera-public/articles/page.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import type { Metadata } from "next";
|
||||
|
||||
import { InnerHeader } from "../_components/InnerHeader";
|
||||
import { PublicLink } from "../_components/PublicLink";
|
||||
import { FooterV3 } from "../_components/v3/FooterV3";
|
||||
import { SERVICE_PRICE_RUB } from "../content";
|
||||
import styles from "../landing-v3.module.css";
|
||||
import { ARTICLE_CARDS } from "./articles-content";
|
||||
|
||||
/**
|
||||
* Список статей — макет 29.08 («МЕРА статьи»).
|
||||
*
|
||||
* ОТЛИЧИЯ ОТ МАКЕТА И ПОЧЕМУ:
|
||||
* - В макете девять карточек и подпись «показано 9 из 9 · новые статьи —
|
||||
* каждую неделю». Написана одна. Остальные две (их рубрики и подводки взяты
|
||||
* из макета) стоят карточками без ссылки с меткой «СКОРО», а обещания
|
||||
* еженедельного выхода нет: расписания публикаций никто не устанавливал.
|
||||
* - Фильтр по рубрикам («Все · Ценообразование · Сроки · Торг · Процесс») не
|
||||
* рендерится: на трёх карточках фильтровать нечего, а неработающий контрол
|
||||
* хуже отсутствующего.
|
||||
* - Шапка — короткая (`InnerHeader`), а не лэндинговая, как в макете:
|
||||
* лэндинговая содержит якоря на секции (`#accuracy`, `#game`), которых на
|
||||
* этой странице нет, и клик по ним никуда бы не привёл.
|
||||
*/
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Статьи — МЕРА · разборы на данных",
|
||||
robots: { index: false, follow: false },
|
||||
};
|
||||
|
||||
export default function ArticlesPage() {
|
||||
return (
|
||||
<>
|
||||
<InnerHeader />
|
||||
<main id="content" className={styles.artPage} tabIndex={-1}>
|
||||
<nav className={styles.artCrumbs} aria-label="Хлебные крошки">
|
||||
<PublicLink to="home" className={styles.artCrumbLink}>
|
||||
Главная
|
||||
</PublicLink>
|
||||
<span aria-hidden="true">/</span>
|
||||
<span aria-current="page">Статьи</span>
|
||||
</nav>
|
||||
|
||||
<header className={styles.artPageHead}>
|
||||
<div className={styles.repEyebrow}>СТАТЬИ И ГАЙДЫ</div>
|
||||
<h1 className={styles.artPageTitle}>
|
||||
Продажа квартиры: разборы на данных, а не на мнениях
|
||||
</h1>
|
||||
<p className={styles.artPageLead}>
|
||||
Как отбирать аналоги, считать цену за метр и проверять её сроком
|
||||
продажи. Каждый разбор заканчивается практическим шагом —
|
||||
бесплатной проверкой вашей квартиры.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className={styles.artGrid}>
|
||||
{ARTICLE_CARDS.map((c) =>
|
||||
c.route ? (
|
||||
<PublicLink
|
||||
key={c.title}
|
||||
to={c.route}
|
||||
className={styles.artCardLink}
|
||||
>
|
||||
<div className={styles.artRubric}>{c.rubric}</div>
|
||||
<div className={styles.artCardTitle}>{c.title}</div>
|
||||
<div className={styles.artCardLead}>{c.lead}</div>
|
||||
<div className={styles.artReadMore}>Читать →</div>
|
||||
</PublicLink>
|
||||
) : (
|
||||
<article key={c.title} className={styles.artCard}>
|
||||
<div className={styles.artRubric}>{c.rubric}</div>
|
||||
<div className={styles.artCardTitle}>{c.title}</div>
|
||||
<div className={styles.artCardLead}>{c.lead}</div>
|
||||
<div className={styles.artSoon}>СКОРО</div>
|
||||
</article>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.artListCta}>
|
||||
<div>
|
||||
<div className={styles.artCtaTitle}>
|
||||
Хватит читать — проверьте свою квартиру
|
||||
</div>
|
||||
<p className={styles.artCtaBody}>
|
||||
Бесплатно: сколько похожих квартир продаётся рядом и насколько
|
||||
свежие это объявления. Полный отчёт — {SERVICE_PRICE_RUB} ₽.
|
||||
</p>
|
||||
</div>
|
||||
<PublicLink to="estimate" className={styles.gameBtnAccent}>
|
||||
Проверить за 30 секунд
|
||||
</PublicLink>
|
||||
</div>
|
||||
</main>
|
||||
<FooterV3 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -191,6 +191,10 @@ export const PUBLIC_ROUTES = {
|
|||
oferta: "/oferta",
|
||||
refund: "/refund",
|
||||
privacy: "/privacy",
|
||||
// Макет 29.08: раздел статей и хаб документов.
|
||||
articles: "/articles",
|
||||
articleHowTo: "/articles/kak-ocenit-kvartiru",
|
||||
docs: "/docs",
|
||||
} as const;
|
||||
|
||||
/** Канонический публичный origin сервиса — база для canonical-ссылок. */
|
||||
|
|
|
|||
191
tradein-mvp/frontend/src/app/mera-public/docs/page.tsx
Normal file
191
tradein-mvp/frontend/src/app/mera-public/docs/page.tsx
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import type { Metadata } from "next";
|
||||
|
||||
import { safeUrl } from "@/lib/safeUrl";
|
||||
|
||||
import { InnerHeader } from "../_components/InnerHeader";
|
||||
import { PublicLink } from "../_components/PublicLink";
|
||||
import { FooterV3 } from "../_components/v3/FooterV3";
|
||||
import {
|
||||
LEGAL_ENTITY,
|
||||
SERVICE_PRICE_RUB,
|
||||
SUPPORT_EMAIL,
|
||||
SUPPORT_TELEGRAM_LABEL,
|
||||
SUPPORT_TELEGRAM_URL,
|
||||
} from "../content";
|
||||
import styles from "../landing-v3.module.css";
|
||||
|
||||
/**
|
||||
* Хаб «Документы и условия» — макет 29.08 («МЕРА документы»).
|
||||
*
|
||||
* ЧЕСТНЫЕ АДАПТАЦИИ:
|
||||
* - Юридические тексты НЕ дублируются и не переписываются макетной редакцией:
|
||||
* источник правды — существующие /oferta, /privacy, /refund (опубликованы
|
||||
* юридической волной 13.08). Хаб только ведёт к ним.
|
||||
* - Реквизиты из макета (ИНН 66700XXXXX, [АДРЕС], mera.example) — маска, а не
|
||||
* данные; до появления настоящих не публикуются — то же решение, что в
|
||||
* FooterV3.
|
||||
* - Тарифная строка «Продажа под ключ» повторяет ПУТЬ 2 лендинга
|
||||
* (`TwoPathsV3`) и ничего сверх него не обещает: формы заявки в продукте
|
||||
* нет, единственный живой канал — поддержка в Telegram. Цена — «по
|
||||
* договору», а не число: тарифа под ключ в коде не существует.
|
||||
* - Часов работы поддержки страница НЕ называет. В макете стояло «по будням
|
||||
* 9:00-19:00», но регламента ответа никто не устанавливал, а срок ответа на
|
||||
* странице документов читается как обязательство.
|
||||
* - Цена расчёта берётся из `SERVICE_PRICE_RUB` — того же литерала, что
|
||||
* назван в оферте (п. 4.1) и политике возврата. Второй раз числом её здесь
|
||||
* писать нельзя: тогда цена в документах и цена в тарифах смогут разойтись.
|
||||
*
|
||||
* ⚠️ ЧЕГО ЭТА СТРАНИЦА НЕ ДОКАЗЫВАЕТ. «Разовый платёж» и «никаких подписок и
|
||||
* автосписаний» описывают порядок оплаты из оферты, а не работающий контур:
|
||||
* приём платежей не подключён (`PUBLIC_ESTIMATE_ENABLED = false`, таблицы
|
||||
* `payments`/`payment_entitlements` на проде пусты, см. TODO в FooterV3).
|
||||
* Пока дерево под noindex, расхождения «написано против доступно» никто не
|
||||
* увидит; открывать страницу наружу можно только вместе с платёжным контуром
|
||||
* (#2896) — либо сняв тарифную таблицу.
|
||||
*/
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Документы и условия — МЕРА",
|
||||
robots: { index: false, follow: false },
|
||||
};
|
||||
|
||||
const TARIFFS: readonly {
|
||||
readonly name: string;
|
||||
readonly note: string;
|
||||
readonly price: string;
|
||||
}[] = [
|
||||
{
|
||||
name: "Бесплатная проверка",
|
||||
note: "Число аналогов рядом и возраст объявлений по дому",
|
||||
price: "0 ₽",
|
||||
},
|
||||
{
|
||||
name: "Полный отчёт",
|
||||
note: "Цена, прогноз срока, диапазон, аналоги, факторы. Разовый платёж",
|
||||
price: `${SERVICE_PRICE_RUB} ₽`,
|
||||
},
|
||||
{
|
||||
name: "Продажа под ключ",
|
||||
note: "Объявление, фото, показы, переговоры, сделка — если вести продажу самому некогда. Условия обсуждаются в поддержке",
|
||||
price: "по договору",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export default function DocsPage() {
|
||||
const telegramHref = safeUrl(SUPPORT_TELEGRAM_URL);
|
||||
|
||||
return (
|
||||
<>
|
||||
<InnerHeader />
|
||||
<main id="content" className={styles.artPage} tabIndex={-1}>
|
||||
<nav className={styles.artCrumbs} aria-label="Хлебные крошки">
|
||||
<PublicLink to="home" className={styles.artCrumbLink}>
|
||||
Главная
|
||||
</PublicLink>
|
||||
<span aria-hidden="true">/</span>
|
||||
<span aria-current="page">Документы</span>
|
||||
</nav>
|
||||
|
||||
<header className={styles.artPageHead}>
|
||||
<div className={styles.repEyebrow}>ДОКУМЕНТЫ</div>
|
||||
<h1 className={styles.artPageTitle}>Документы и условия</h1>
|
||||
</header>
|
||||
|
||||
<section className={styles.docsSection} aria-labelledby="docs-legal">
|
||||
<h2 id="docs-legal" className={styles.docsH2}>
|
||||
Юридические документы
|
||||
</h2>
|
||||
<div className={styles.docsLinks}>
|
||||
<PublicLink to="oferta" className={styles.docsLinkCard}>
|
||||
<span className={styles.docsLinkTitle}>Публичная оферта</span>
|
||||
<span className={styles.docsLinkNote}>
|
||||
Договор оказания информационных услуг
|
||||
</span>
|
||||
</PublicLink>
|
||||
<PublicLink to="privacy" className={styles.docsLinkCard}>
|
||||
<span className={styles.docsLinkTitle}>Персональные данные</span>
|
||||
<span className={styles.docsLinkNote}>
|
||||
Политика обработки и ваши права по 152-ФЗ
|
||||
</span>
|
||||
</PublicLink>
|
||||
<PublicLink to="refund" className={styles.docsLinkCard}>
|
||||
<span className={styles.docsLinkTitle}>Возврат оплаты</span>
|
||||
<span className={styles.docsLinkNote}>
|
||||
Когда и как возвращаются {SERVICE_PRICE_RUB} ₽
|
||||
</span>
|
||||
</PublicLink>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.docsSection} aria-labelledby="docs-tariffs">
|
||||
<h2 id="docs-tariffs" className={styles.docsH2}>
|
||||
Тарифы
|
||||
</h2>
|
||||
<p className={styles.artP}>
|
||||
Никаких подписок и автосписаний. Все цены — окончательные.
|
||||
</p>
|
||||
<div className={styles.artTable} role="table">
|
||||
<div className={styles.artTableHead} role="row">
|
||||
<span role="columnheader">УСЛУГА</span>
|
||||
<span role="columnheader" className={styles.artTableRight}>
|
||||
ЦЕНА
|
||||
</span>
|
||||
</div>
|
||||
{TARIFFS.map((t) => (
|
||||
<div key={t.name} className={styles.artTableRow} role="row">
|
||||
<span role="cell">
|
||||
<span className={styles.docsTariffName}>{t.name}</span>
|
||||
<span className={styles.docsTariffNote}>{t.note}</span>
|
||||
</span>
|
||||
<span role="cell" className={styles.artTableRight}>
|
||||
{t.price}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.docsSection} aria-labelledby="docs-reqs">
|
||||
<h2 id="docs-reqs" className={styles.docsH2}>
|
||||
Контакты и реквизиты
|
||||
</h2>
|
||||
{LEGAL_ENTITY ? (
|
||||
<div className={styles.artKv}>
|
||||
<div className={styles.artKvRow}>
|
||||
<span className={styles.artKvKey}>ОРГАНИЗАЦИЯ</span>
|
||||
<span className={styles.artKvVal}>{LEGAL_ENTITY.name}</span>
|
||||
</div>
|
||||
<div className={styles.artKvRow}>
|
||||
<span className={styles.artKvKey}>ИНН · ОГРН</span>
|
||||
<span className={styles.artKvVal}>
|
||||
{LEGAL_ENTITY.inn} · {LEGAL_ENTITY.ogrn}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.artKvRow}>
|
||||
<span className={styles.artKvKey}>АДРЕС</span>
|
||||
<span className={styles.artKvVal}>{LEGAL_ENTITY.address}</span>
|
||||
</div>
|
||||
<div className={styles.artKvRow}>
|
||||
<span className={styles.artKvKey}>ПОЧТА</span>
|
||||
<span className={styles.artKvVal}>
|
||||
<a href={`mailto:${SUPPORT_EMAIL}`}>{SUPPORT_EMAIL}</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{telegramHref ? (
|
||||
<p className={styles.artP}>
|
||||
Поддержка в Telegram:{" "}
|
||||
<a href={telegramHref} target="_blank" rel="noreferrer">
|
||||
{SUPPORT_TELEGRAM_LABEL}
|
||||
</a>
|
||||
. Это же единственный канал для вопросов по возврату и по продаже
|
||||
под ключ.
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
</main>
|
||||
<FooterV3 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import type { Metadata } from "next";
|
||||
|
||||
import { EstimateFlow } from "../_components/estimate/EstimateFlow";
|
||||
import { EstimateHeader } from "../_components/estimate/EstimateHeader";
|
||||
import { InnerHeader } from "../_components/InnerHeader";
|
||||
import { FooterV3 } from "../_components/v3/FooterV3";
|
||||
import { PUBLIC_ESTIMATE_URL } from "../content";
|
||||
import styles from "../landing-v3.module.css";
|
||||
|
|
@ -35,7 +35,7 @@ export const metadata: Metadata = {
|
|||
export default function MeraEstimatePage() {
|
||||
return (
|
||||
<>
|
||||
<EstimateHeader />
|
||||
<InnerHeader />
|
||||
<main id="content" className={styles.estSection} tabIndex={-1}>
|
||||
<div className={styles.estContainer}>
|
||||
<p className={styles.heroResultEyebrow}>БЕСПЛАТНАЯ ПРОВЕРКА</p>
|
||||
|
|
|
|||
|
|
@ -56,6 +56,10 @@
|
|||
align-items: center;
|
||||
gap: 12px;
|
||||
color: var(--b2c-ink);
|
||||
/* На лэндинге логотип — <div>, на внутренних страницах (InnerHeader) —
|
||||
ссылка на главную; без сброса подчёркивание браузера рисовалось прямо
|
||||
по слову «МЕРА». */
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.hdrLogoIcon {
|
||||
|
|
@ -1812,6 +1816,9 @@
|
|||
text-align: center;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
/* На лэндинге это <button>, в статье и списке статей — ссылка на /estimate:
|
||||
без сброса браузер подчёркивает подпись прямо по кнопке. */
|
||||
text-decoration: none;
|
||||
}
|
||||
.gameBtnAccent:hover {
|
||||
background: var(--b2c-ink);
|
||||
|
|
@ -2410,3 +2417,418 @@
|
|||
line-height: 1.55;
|
||||
color: #9fb0b4;
|
||||
}
|
||||
|
||||
/* --- Раздел статей и хаб документов (макет 29.08) ------------------------- */
|
||||
.artPage {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: clamp(32px, 5vw, 56px) clamp(20px, 5vw, 56px) clamp(56px, 7vw, 88px);
|
||||
}
|
||||
.artPageHead {
|
||||
margin-bottom: clamp(28px, 4vw, 40px);
|
||||
}
|
||||
.artPageTitle {
|
||||
margin: 0;
|
||||
font-size: clamp(28px, 4.2vw, 44px);
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.02em;
|
||||
font-weight: 800;
|
||||
text-wrap: balance;
|
||||
}
|
||||
.artPageLead {
|
||||
margin: 16px 0 0;
|
||||
font-size: 16.5px;
|
||||
line-height: 1.6;
|
||||
color: var(--b2c-muted);
|
||||
max-width: 640px;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.artAllLink {
|
||||
font-weight: 700;
|
||||
font-size: 14.5px;
|
||||
white-space: nowrap;
|
||||
color: var(--b2c-accent-text);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
.artAllLink:hover {
|
||||
border-bottom-color: var(--b2c-accent-text);
|
||||
}
|
||||
.artCardLink {
|
||||
composes: artCard;
|
||||
display: block;
|
||||
color: var(--b2c-ink);
|
||||
text-decoration: none;
|
||||
}
|
||||
.artCardLink:hover {
|
||||
border-color: var(--b2c-accent);
|
||||
color: var(--b2c-ink);
|
||||
}
|
||||
.artSoon {
|
||||
margin-top: 14px;
|
||||
font-family: var(--b2c-font-mono);
|
||||
font-size: 9.5px;
|
||||
letter-spacing: 0.16em;
|
||||
color: #9fb0b4;
|
||||
}
|
||||
.artToc {
|
||||
margin-bottom: clamp(24px, 3vw, 32px);
|
||||
padding: 18px 22px;
|
||||
background: var(--b2c-surface);
|
||||
border: 1px solid var(--b2c-line);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.artTocLabel {
|
||||
margin-bottom: 12px;
|
||||
font-family: var(--b2c-font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.16em;
|
||||
color: var(--b2c-muted);
|
||||
}
|
||||
.artTocList a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid var(--b2c-line);
|
||||
}
|
||||
.artTocList a:hover {
|
||||
color: var(--b2c-accent-text);
|
||||
border-bottom-color: var(--b2c-accent-text);
|
||||
}
|
||||
.artTocList {
|
||||
margin: 0;
|
||||
padding: 0 0 0 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
font-size: 14.5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.artBody {
|
||||
max-width: 760px;
|
||||
}
|
||||
.artP {
|
||||
margin: 0 0 16px;
|
||||
font-size: 16px;
|
||||
line-height: 1.7;
|
||||
color: #2c3a3f;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.artH2 {
|
||||
margin: clamp(30px, 4vw, 42px) 0 14px;
|
||||
font-size: clamp(21px, 2.6vw, 27px);
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.015em;
|
||||
font-weight: 800;
|
||||
scroll-margin-top: 90px;
|
||||
}
|
||||
.artMemo {
|
||||
margin: 20px 0;
|
||||
padding: 18px 22px;
|
||||
border-left: 3px solid var(--b2c-accent);
|
||||
background: var(--b2c-surface);
|
||||
border-radius: 0 10px 10px 0;
|
||||
}
|
||||
.artMemoLabel {
|
||||
margin-bottom: 8px;
|
||||
font-family: var(--b2c-font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.16em;
|
||||
color: var(--b2c-accent-text);
|
||||
}
|
||||
.artMemo p {
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
font-size: 15.5px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.artKv {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin: 18px 0;
|
||||
}
|
||||
.artKvRow {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
align-items: baseline;
|
||||
padding: 13px 18px;
|
||||
background: var(--b2c-surface);
|
||||
border: 1px solid var(--b2c-line);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.artKvKey {
|
||||
flex: none;
|
||||
width: 110px;
|
||||
font-family: var(--b2c-font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--b2c-accent-text);
|
||||
}
|
||||
.artKvVal {
|
||||
font-size: 14.5px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.artTable {
|
||||
margin: 18px 0 10px;
|
||||
border: 1px solid var(--b2c-line);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.artTableHead {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 12px 20px;
|
||||
background: var(--b2c-surface-soft);
|
||||
font-family: var(--b2c-font-mono);
|
||||
font-size: 9.5px;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--b2c-muted);
|
||||
}
|
||||
.artTableRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 14px 20px;
|
||||
border-top: 1px solid var(--b2c-line-soft);
|
||||
background: var(--b2c-surface);
|
||||
font-size: 14.5px;
|
||||
align-items: baseline;
|
||||
}
|
||||
.artTableRight {
|
||||
flex: none;
|
||||
font-family: var(--b2c-font-mono);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.artTableRow .artTableRight {
|
||||
color: var(--b2c-accent-text);
|
||||
}
|
||||
.artTableHead .artTableRight {
|
||||
color: inherit;
|
||||
}
|
||||
.artFootnote {
|
||||
margin-bottom: 8px;
|
||||
font-family: var(--b2c-font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.08em;
|
||||
color: #9fb0b4;
|
||||
}
|
||||
.artTiles {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
|
||||
gap: 14px;
|
||||
margin: 18px 0;
|
||||
}
|
||||
.artTile {
|
||||
padding: 20px 22px;
|
||||
background: var(--b2c-surface);
|
||||
border: 1px solid var(--b2c-line);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.artTileValue {
|
||||
font-size: clamp(24px, 2.8vw, 30px);
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1;
|
||||
}
|
||||
.artTileLabel {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--b2c-muted);
|
||||
}
|
||||
.artCta {
|
||||
margin: clamp(26px, 4vw, 38px) 0;
|
||||
padding: clamp(24px, 3.5vw, 36px);
|
||||
background: var(--b2c-surface-dark);
|
||||
border-radius: 14px;
|
||||
color: #ffffff;
|
||||
}
|
||||
.artCtaEyebrow {
|
||||
margin-bottom: 12px;
|
||||
font-family: var(--b2c-font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.16em;
|
||||
color: var(--b2c-accent);
|
||||
}
|
||||
.artCtaTitle {
|
||||
margin-bottom: 10px;
|
||||
font-weight: 800;
|
||||
font-size: clamp(20px, 2.4vw, 25px);
|
||||
}
|
||||
.artCtaBody {
|
||||
margin: 0 0 20px;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
color: #9fb0b4;
|
||||
max-width: 520px;
|
||||
}
|
||||
.artCta .gameBtnAccent {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
}
|
||||
.artMistakes {
|
||||
margin: 18px 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.artMistake {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 16px 20px;
|
||||
background: var(--b2c-surface);
|
||||
border: 1px solid var(--b2c-line);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.artMistakeNum {
|
||||
flex: none;
|
||||
font-family: var(--b2c-font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--b2c-accent-text);
|
||||
padding-top: 4px;
|
||||
}
|
||||
.artMistakeTitle {
|
||||
font-weight: 800;
|
||||
font-size: 15.5px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.artMistakeBody {
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
color: var(--b2c-muted);
|
||||
}
|
||||
.artSummary {
|
||||
margin-top: clamp(26px, 4vw, 38px);
|
||||
padding-top: 22px;
|
||||
border-top: 1px solid var(--b2c-line);
|
||||
}
|
||||
.artSummary p {
|
||||
margin: 0;
|
||||
font-size: 15.5px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.artMore {
|
||||
margin-top: clamp(40px, 6vw, 64px);
|
||||
}
|
||||
.artListCta {
|
||||
margin-top: clamp(32px, 5vw, 48px);
|
||||
padding: clamp(24px, 3.5vw, 36px);
|
||||
/* Тёмная плашка — как в макете: это последний экран списка, и он обязан
|
||||
читаться как призыв, а не как ещё одна карточка статьи. */
|
||||
background: var(--b2c-surface-dark);
|
||||
border-radius: 14px;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.artListCta .artCtaBody {
|
||||
margin: 10px 0 0;
|
||||
max-width: 460px;
|
||||
}
|
||||
.artCrumbs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 18px;
|
||||
font-size: 13px;
|
||||
color: var(--b2c-muted);
|
||||
}
|
||||
.artCrumbLink {
|
||||
color: var(--b2c-muted);
|
||||
text-decoration: none;
|
||||
}
|
||||
.artCrumbLink:hover {
|
||||
color: var(--b2c-accent-text);
|
||||
}
|
||||
.artCardLead {
|
||||
margin-top: 10px;
|
||||
font-size: 13.5px;
|
||||
line-height: 1.55;
|
||||
color: var(--b2c-muted);
|
||||
}
|
||||
.artReadMore {
|
||||
margin-top: 14px;
|
||||
font-weight: 700;
|
||||
font-size: 13.5px;
|
||||
color: var(--b2c-accent-text);
|
||||
}
|
||||
.artListCta .gameBtnAccent {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
}
|
||||
.docsSection {
|
||||
margin-bottom: clamp(30px, 4vw, 44px);
|
||||
}
|
||||
.docsH2 {
|
||||
margin: 0 0 14px;
|
||||
font-size: clamp(19px, 2.3vw, 24px);
|
||||
letter-spacing: -0.01em;
|
||||
font-weight: 800;
|
||||
}
|
||||
.docsLinks {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
.docsLinkCard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 20px 22px;
|
||||
background: var(--b2c-surface);
|
||||
border: 1px solid var(--b2c-line);
|
||||
border-radius: 10px;
|
||||
color: var(--b2c-ink);
|
||||
text-decoration: none;
|
||||
}
|
||||
.docsLinkCard:hover {
|
||||
border-color: var(--b2c-accent);
|
||||
color: var(--b2c-ink);
|
||||
}
|
||||
.docsLinkTitle {
|
||||
font-weight: 800;
|
||||
font-size: 16px;
|
||||
}
|
||||
.docsLinkNote {
|
||||
font-size: 13.5px;
|
||||
color: var(--b2c-muted);
|
||||
}
|
||||
.docsTariffName {
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
}
|
||||
.docsTariffNote {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
font-size: 13px;
|
||||
color: var(--b2c-muted);
|
||||
}
|
||||
|
||||
/* Узкий экран: ключ пары перестаёт быть колонкой фиксированной ширины —
|
||||
иначе на 320px значению остаётся ~170px и «−5…−8%» рвётся переносом. */
|
||||
@media (max-width: 480px) {
|
||||
.artKvRow {
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.artKvKey {
|
||||
width: auto;
|
||||
}
|
||||
.artTableRow,
|
||||
.artTableHead {
|
||||
gap: 10px;
|
||||
}
|
||||
.artListCta {
|
||||
display: block;
|
||||
}
|
||||
.artListCta .gameBtnAccent {
|
||||
margin-top: 18px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,17 @@ const PUBLIC_PATHS = ["/mera-public"];
|
|||
* Совпадение теперь запинено тестом (`__tests__/RouteGuard.publicPaths.test.ts`
|
||||
* читает Caddyfile), потому что комментарий этот инцидент уже не предотвратил.
|
||||
*/
|
||||
const PUBLIC_SHORT_PATHS = ["/", "/estimate", "/oferta", "/refund", "/privacy", "/v3"];
|
||||
const PUBLIC_SHORT_PATHS = [
|
||||
"/",
|
||||
"/estimate",
|
||||
"/oferta",
|
||||
"/refund",
|
||||
"/privacy",
|
||||
"/v3",
|
||||
"/articles",
|
||||
"/articles/kak-ocenit-kvartiru",
|
||||
"/docs",
|
||||
];
|
||||
|
||||
export function isPublicPath(rawPath: string): boolean {
|
||||
// Точное совпадение — ОТДЕЛЬНОЙ веткой и первым делом. Префиксную проверку
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue