From be7d7d0205ef0cac37aba1a5cbedccda31f193c9 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sat, 29 Aug 2026 16:06:56 +0500 Subject: [PATCH] =?UTF-8?q?feat(mera/b2c):=20=D0=BB=D1=8D=D0=BD=D0=B4?= =?UTF-8?q?=D0=B8=D0=BD=D0=B3=20v3=20=D0=B4=D0=BE=D0=B2=D0=B5=D0=B4=D1=91?= =?UTF-8?q?=D0=BD=20=D0=B4=D0=BE=20=D0=BC=D0=B0=D0=BA=D0=B5=D1=82=D0=B0=20?= =?UTF-8?q?29.08=20=E2=80=94=20=D0=BB=D0=B5=D0=BD=D1=82=D0=B0,=20=D0=B8?= =?UTF-8?q?=D0=B3=D1=80=D0=B0,=20=D0=B4=D0=B2=D0=B0=20=D0=BF=D1=83=D1=82?= =?UTF-8?q?=D0=B8,=20=D0=B2=D0=BE=D0=B7=D1=80=D0=B0=D0=B6=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F,=20=D1=81=D1=82=D0=B0=D1=82=D1=8C=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Макет «B2C модуль для МЕРА» (Макс, 29.08) — пять секций, которых превью /mera-public/v3 не имело (его шапка их и перечисляла как отсутствующие): - DealsTickerV3 — бегущая строка «ПРОГНОЗ → ФАКТ»; данные — ТЕ ЖЕ строки, что таблица сверок (PROOF_ROWS_PLACEHOLDER): один будущий контур — один набор реквизита, второй не заводится; - GuessGameV3 — игра «Угадай цену», три раунда со слайдером, сверка «ваш ответ / МЕРА / факт», итог с медианной ошибкой игрока; раунды — GAME_ROUNDS_PLACEHOLDER под гейтом #2904; - TwoPathsV3 — «сами / сделаем за вас»; заявка пути 2 ведёт в Telegram поддержки (формы и договора «под ключ» не существует); - ObjectionsV3 — FAQ «перед оплатой» на нативных details; ответы из выверенной честной копии (content.ts/oferta/refund), макетный ответ про «отметки продавцов» заменён тем, что в продукте есть (Росреестр + снятие с публикации); - ArticlesV3 — тизер «Разборы на данных»; карточки БЕЗ ссылок до появления раздела статей (следующий шард). Решение «модалка → /estimate» (StickyCtaV3) сохранено для всех CTA макета. Гейт noindex↔плейсхолдеры дополнен новыми файлами и GAME_ROUNDS_PLACEHOLDER. Проверено: tsc чисто, next lint чисто, vitest 27 passed, живой прогон игры на dev (раунд → сверка → следующая), полностраничный скрин 1:1 с макетом. Co-Authored-By: Claude Opus 5 --- .../marketing-placeholders-gate.test.ts | 4 + .../mera-public/_components/v3/ArticlesV3.tsx | 51 ++ .../_components/v3/DealsTickerV3.tsx | 52 ++ .../_components/v3/GuessGameV3.tsx | 214 ++++++ .../_components/v3/ObjectionsV3.tsx | 97 +++ .../mera-public/_components/v3/TwoPathsV3.tsx | 73 ++ .../src/app/mera-public/landing-v3.module.css | 682 ++++++++++++++++++ .../src/app/mera-public/marketing-v3.ts | 83 +++ .../frontend/src/app/mera-public/v3/page.tsx | 33 +- 9 files changed, 1279 insertions(+), 10 deletions(-) create mode 100644 tradein-mvp/frontend/src/app/mera-public/_components/v3/ArticlesV3.tsx create mode 100644 tradein-mvp/frontend/src/app/mera-public/_components/v3/DealsTickerV3.tsx create mode 100644 tradein-mvp/frontend/src/app/mera-public/_components/v3/GuessGameV3.tsx create mode 100644 tradein-mvp/frontend/src/app/mera-public/_components/v3/ObjectionsV3.tsx create mode 100644 tradein-mvp/frontend/src/app/mera-public/_components/v3/TwoPathsV3.tsx diff --git a/tradein-mvp/frontend/src/app/mera-public/__tests__/marketing-placeholders-gate.test.ts b/tradein-mvp/frontend/src/app/mera-public/__tests__/marketing-placeholders-gate.test.ts index 8f6ed5d1..cd7441a6 100644 --- a/tradein-mvp/frontend/src/app/mera-public/__tests__/marketing-placeholders-gate.test.ts +++ b/tradein-mvp/frontend/src/app/mera-public/__tests__/marketing-placeholders-gate.test.ts @@ -41,6 +41,9 @@ const V3_SOURCES = [ "_components/v3/AccuracyV3.tsx", "_components/v3/FreeCheckCard.tsx", "_components/v3/FreeResultV3.tsx", + // Макет 29.08: лента и игра тоже стоят на реквизитных величинах. + "_components/v3/DealsTickerV3.tsx", + "_components/v3/GuessGameV3.tsx", ]; const PLACEHOLDER_EXPORTS = [ @@ -48,6 +51,7 @@ const PLACEHOLDER_EXPORTS = [ "COST_OF_ERROR_PLACEHOLDER", "FREE_SAMPLE_PLACEHOLDER", "PROOF_ROWS_PLACEHOLDER", + "GAME_ROUNDS_PLACEHOLDER", ]; function placeholdersInUse(): string[] { diff --git a/tradein-mvp/frontend/src/app/mera-public/_components/v3/ArticlesV3.tsx b/tradein-mvp/frontend/src/app/mera-public/_components/v3/ArticlesV3.tsx new file mode 100644 index 00000000..e3dc1102 --- /dev/null +++ b/tradein-mvp/frontend/src/app/mera-public/_components/v3/ArticlesV3.tsx @@ -0,0 +1,51 @@ +/** + * ArticlesV3 — «Разборы на данных» (макет 29.08, секция «V3 — статьи»). + * + * Раздела статей в продукте НЕ СУЩЕСТВУЕТ (страницы «МЕРА статьи»/«МЕРА + * статья» из макета — следующий шард). Поэтому карточки рендерятся
, + * а НЕ : ссылка «в никуда» на превью читалась бы как сломанная навигация. + * Ссылка «Все статьи →» по той же причине не рендерится вовсе — появится + * вместе с разделом. Темы и рубрики карточек — из макета дословно. + */ + +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: "Завысить и торговаться или поставить в рынок: что выгоднее", + }, +]; + +export function ArticlesV3() { + return ( +
+
+

+ Разборы на данных +

+
+
+ {ARTICLES.map((a) => ( +
+
{a.rubric}
+
{a.title}
+
+ ))} +
+
+ ); +} diff --git a/tradein-mvp/frontend/src/app/mera-public/_components/v3/DealsTickerV3.tsx b/tradein-mvp/frontend/src/app/mera-public/_components/v3/DealsTickerV3.tsx new file mode 100644 index 00000000..7d42cb50 --- /dev/null +++ b/tradein-mvp/frontend/src/app/mera-public/_components/v3/DealsTickerV3.tsx @@ -0,0 +1,52 @@ +/** + * DealsTickerV3 — бегущая строка «ПРОГНОЗ → ФАКТ» (макет 29.08, секция + * «V3 — лента сделок»): тёмная полоса сразу под шапкой. + * + * Данные — ТЕ ЖЕ строки, что в таблице сверок (`PROOF_ROWS_PLACEHOLDER`): + * лента и таблица иллюстрируют один и тот же будущий контур «прогноз → факт», + * которого в продукте пока нет — заводить второй набор выдуманных сделок + * значило бы удвоить сущности под одним и тем же гейтом (#2904). Список + * дублируется дважды подряд — стандартный приём бесшовного CSS-marquee + * (анимация уводит ровно на −50%). + * + * `aria-hidden` на второй копии и `role="marquee"`-семантика намеренно НЕ + * используются: полоса декоративна, весь её смысл продублирован таблицей + * сверок ниже по странице — скринридеру достаточно одного источника. + * `prefers-reduced-motion` глушится глобальным правилом из css-модуля. + */ + +import { PROOF_ROWS_PLACEHOLDER } from "../../marketing-v3"; +import styles from "../../landing-v3.module.css"; + +export function DealsTickerV3() { + const items = [...PROOF_ROWS_PLACEHOLDER, ...PROOF_ROWS_PLACEHOLDER]; + return ( +
+
+
+ +
+ ); +} diff --git a/tradein-mvp/frontend/src/app/mera-public/_components/v3/GuessGameV3.tsx b/tradein-mvp/frontend/src/app/mera-public/_components/v3/GuessGameV3.tsx new file mode 100644 index 00000000..a5a4a74d --- /dev/null +++ b/tradein-mvp/frontend/src/app/mera-public/_components/v3/GuessGameV3.tsx @@ -0,0 +1,214 @@ +"use client"; + +/** + * GuessGameV3 — мини-игра «Сыграйте против МЕРЫ на реальных сделках» + * (макет 29.08, секция `#game` «V3 — игра "Угадай цену"»). + * + * Клиентский компонент: три раунда «слайдер → ответ → сверка», итоговый экран + * со средней ошибкой игрока против ошибки МЕРЫ. Раунды — реквизит из + * `GAME_ROUNDS_PLACEHOLDER` (см. шапку marketing-v3.ts: контура сверки + * «прогноз → факт» в продукте нет, «реальные сделки» здесь — макетные, и + * страница закрыта noindex до появления вычисляемых). + * + * Ошибка игрока считается медианой |ответ − факт| / факт по трём раундам — + * та же метрика, которой описана ошибка МЕРЫ (GAME_MERA_ERROR_LABEL). + * Финальная CTA ведёт на `/estimate` (решение StickyCtaV3 вместо макетной + * модалки). + */ + +import { useState } from "react"; + +import { + GAME_MERA_ERROR_LABEL, + GAME_ROUNDS_PLACEHOLDER, +} from "../../marketing-v3"; +import styles from "../../landing-v3.module.css"; + +import { PublicLink } from "../PublicLink"; + +type Phase = "playing" | "revealed" | "finished"; + +const fmtMln = (v: number) => + `${v.toFixed(2).replace(".", ",")} млн ₽`; + +function median(xs: number[]): number { + const s = [...xs].sort((a, b) => a - b); + const mid = Math.floor(s.length / 2); + return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2; +} + +export function GuessGameV3() { + const rounds = GAME_ROUNDS_PLACEHOLDER; + const [idx, setIdx] = useState(0); + const [phase, setPhase] = useState("playing"); + const [guess, setGuess] = useState(rounds[0].start); + const [errors, setErrors] = useState([]); + + const r = rounds[idx]; + const isLast = idx === rounds.length - 1; + + const answer = () => { + setErrors((prev) => [...prev, Math.abs(guess - r.fact) / r.fact]); + setPhase("revealed"); + }; + const next = () => { + if (isLast) { + setPhase("finished"); + return; + } + const n = idx + 1; + setIdx(n); + setGuess(rounds[n].start); + setPhase("playing"); + }; + const restart = () => { + setIdx(0); + setGuess(rounds[0].start); + setErrors([]); + setPhase("playing"); + }; + + const userErrPct = errors.length + ? `${(median(errors) * 100).toFixed(1).replace(".", ",")}%` + : "—"; + const roundErrPct = errors.length + ? `${(errors[errors.length - 1] * 100).toFixed(1).replace(".", ",")}%` + : "—"; + + return ( +
+
+
+ ПРОВЕРЬТЕ СЕБЯ · РАУНД {Math.min(idx + 1, rounds.length)} ИЗ {rounds.length} +
+

+ Сыграйте против МЕРЫ на реальных сделках +

+

+ Три квартиры, которые уже проданы. Назовите свою цифру — потом + увидите, что сказала МЕРА и чем всё закончилось. +

+
+ +
+
+
+ +
+ ); +} diff --git a/tradein-mvp/frontend/src/app/mera-public/_components/v3/ObjectionsV3.tsx b/tradein-mvp/frontend/src/app/mera-public/_components/v3/ObjectionsV3.tsx new file mode 100644 index 00000000..99f7cece --- /dev/null +++ b/tradein-mvp/frontend/src/app/mera-public/_components/v3/ObjectionsV3.tsx @@ -0,0 +1,97 @@ +/** + * ObjectionsV3 — «Что обычно спрашивают перед оплатой» (макет 29.08, секция + * «V3 — возражения»): двухколоночный FAQ с CTA слева. + * + * Аккордеон — нативные
/, как в Faq старого лэндинга: + * макетный клиентский toggle не нужен, поведение и клавиатурная доступность + * бесплатны. Иконка +/− рисуется через CSS по [open]. + * + * Ответы НЕ из макета (там реквизит рендера): собраны из уже выверенной + * честной копии продукта — content.ts (FAQ), oferta/refund. Вопрос про факт + * сделки отвечаем тем, что в продукте ЕСТЬ (сделки Росреестра + снятие + * объявлений с публикации), а не макетной формулировкой про «отметки + * продавцов», механики которых не существует. + */ + +import styles from "../../landing-v3.module.css"; + +import { PublicLink } from "../PublicLink"; + +interface Objection { + readonly q: string; + readonly a: readonly string[]; +} + +const OBJECTIONS: readonly Objection[] = [ + { + q: "Откуда вы знаете факт сделки?", + a: [ + "К объявлениям мы добавляем зарегистрированные сделки Росреестра и отслеживаем снятие объявлений с публикации. По этим данным считается расхождение прогноза с реальностью — и честно показывается, на скольких объектах построен каждый расчёт.", + ], + }, + { + q: "Почему так дёшево — это точно работает?", + a: [ + "Дёшево, потому что расчёт автоматический: без выезда, без оценщика, без комиссии. Вы платите за данные, а не за часы специалиста.", + "Точность прямо зависит от того, сколько нашлось сопоставимых объектов, — поэтому мы всегда пишем, на скольких объектах и сделках построен расчёт, и честно сообщаем, если данных по дому мало.", + ], + }, + { + q: "Чем это лучше бесплатных калькуляторов?", + a: [ + "Калькулятор площадки считает по объявлениям этой же площадки — то есть по ценам, которые продавцы просят, а не получают.", + "Мы смотрим сразу несколько площадок и добавляем зарегистрированные сделки, чтобы была видна разница между запрашиваемой и реальной ценой.", + ], + }, + { + q: "Это подписка? Спишут ли ещё раз?", + a: [ + "Нет. Это разовый платёж за один отчёт — без подписки, автопродлений и скрытых списаний. Отчёт остаётся у вас по ссылке.", + ], + }, + { + q: "Мне начнут звонить после проверки?", + a: [ + "Нет. Для расчёта нужны адрес и параметры квартиры — телефон мы не спрашиваем. Он появляется, только если вы сами решите оставить заявку, и всегда с отдельной галочкой согласия.", + ], + }, + { + q: "Это официальная оценка для банка?", + a: [ + "Нет. Для ипотеки, суда, опеки и нотариуса нужен отчёт аккредитованного оценщика по 135-ФЗ — это отдельная процедура с выездом. МЕРА отвечает на другой вопрос: за сколько квартира реально продаётся на рынке сегодня.", + ], + }, +]; + +export function ObjectionsV3() { + return ( +
+
+
+
СОМНЕНИЯ
+

+ Что обычно спрашивают перед оплатой +

+ + Проверить за 30 секунд + +
+
+ {OBJECTIONS.map((o) => ( +
+ + {o.q} + +
+ {o.a.map((p) => ( +

{p}

+ ))} +
+
+ ))} +
+
+
+ ); +} diff --git a/tradein-mvp/frontend/src/app/mera-public/_components/v3/TwoPathsV3.tsx b/tradein-mvp/frontend/src/app/mera-public/_components/v3/TwoPathsV3.tsx new file mode 100644 index 00000000..8e483dd4 --- /dev/null +++ b/tradein-mvp/frontend/src/app/mera-public/_components/v3/TwoPathsV3.tsx @@ -0,0 +1,73 @@ +/** + * TwoPathsV3 — «С отчётом справитесь сами. Не уверены — сделаем за вас» + * (макет 29.08, секция `#turnkey` «V3 — два пути»). + * + * Кнопка пути 1 ведёт на `/estimate` — то же решение, что у StickyCtaV3: + * макетный `openModal` в коде заменён отдельным экраном проверки. + * + * Кнопка пути 2 («Оставить заявку») ведёт в Telegram поддержки: формы + * партнёрской заявки и договора «продажи под ключ» в продукте НЕТ — до их + * появления честный путь заявки один, живой канал. Текст пути 2 из макета + * сохранён дословно: он аккуратно не обещает ничего сверх «узнайте условия». + */ + +import { SUPPORT_TELEGRAM_URL } from "../../content"; +import { COST_OF_ERROR_PLACEHOLDER } from "../../marketing-v3"; +import styles from "../../landing-v3.module.css"; + +import { PublicLink } from "../PublicLink"; + +const REPORT_PRICE = COST_OF_ERROR_PLACEHOLDER[2].value; + +export function TwoPathsV3() { + return ( +
+
+
ВЫБЕРИТЕ ПУТЬ
+

+ С отчётом справитесь сами. Не уверены — сделаем за вас +

+
+
+
+
ПУТЬ 1 · САМОСТОЯТЕЛЬНО
+
Отчёт за {REPORT_PRICE}
+

+ Цена, срок, аналоги, факторы. Дальше вы ведёте продажу сами — на + руках все цифры, чтобы уверенно держать свою цену в переговорах. +

+ + Проверить квартиру + +
+
+
+
+ ); +} diff --git a/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css b/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css index a55b1c92..eb9cb4ca 100644 --- a/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css +++ b/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css @@ -1482,3 +1482,685 @@ border-radius: 12px; background: var(--b2c-surface-soft); } + +/* ========================================================================== + Макет 29.08 («B2C модуль», Макс): лента сделок, игра, два пути, + возражения, статьи. Та же палитра b2c-tokens, что и выше. + ========================================================================== */ + +/* --- Лента «ПРОГНОЗ → ФАКТ» ---------------------------------------------- */ +.tickerRoot { + position: relative; + overflow: hidden; + background: var(--b2c-surface-dark); +} +.tickerBadge { + position: absolute; + inset: 0 auto 0 0; + z-index: 2; + display: flex; + align-items: center; + gap: 10px; + padding: 0 18px 0 clamp(20px, 5vw, 56px); + background: var(--b2c-surface-dark); + box-shadow: 18px 0 24px rgba(18, 26, 29, 0.9); +} +.tickerDot { + display: block; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--b2c-accent); + animation: tickerPulse 2s ease-in-out infinite; +} +.tickerBadgeText { + font-family: var(--b2c-font-mono); + font-size: 10px; + letter-spacing: 0.16em; + color: var(--b2c-accent); + white-space: nowrap; +} +.tickerTrack { + display: flex; + width: max-content; + padding: 12px 0; + animation: tickerMarquee 90s linear infinite; +} +.tickerItem { + display: flex; + align-items: center; + gap: 20px; + padding: 0 30px; + border-right: 1px solid #2c3a3f; + white-space: nowrap; +} +.tickerObj { + font-size: 15px; + font-weight: 800; + color: #ffffff; +} +.tickerPair { + display: flex; + align-items: center; + gap: 8px; +} +.tickerLabelMuted { + font-family: var(--b2c-font-mono); + font-size: 10px; + letter-spacing: 0.12em; + color: var(--b2c-muted); +} +.tickerLabelAccent { + composes: tickerLabelMuted; + color: var(--b2c-accent-text); +} +.tickerSaid { + font-size: 14px; + font-weight: 600; + color: #9fb0b4; +} +.tickerArrow { + color: #2c3a3f; + font-size: 15px; +} +.tickerFact { + font-size: 14px; + font-weight: 700; + color: var(--b2c-accent); +} +.tickerErr { + font-family: var(--b2c-font-mono); + font-size: 11px; + color: #9fb0b4; +} +.tickerGain { + display: flex; + align-items: center; + gap: 5px; + font-family: var(--b2c-font-mono); + font-size: 13px; + font-weight: 500; + color: #3fd1a6; +} +.tickerGainCaret { + font-size: 10px; +} +@keyframes tickerMarquee { + from { transform: translateX(0); } + to { transform: translateX(-50%); } +} +@keyframes tickerPulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.45; } +} +@media (prefers-reduced-motion: reduce) { + .tickerTrack { animation: none; } + .tickerDot { animation: none; } +} + +/* --- Игра «Угадай цену» --------------------------------------------------- */ +.gameSection { + max-width: 1280px; + margin: 0 auto; + padding: clamp(48px, 6vw, 80px) clamp(20px, 5vw, 56px); +} +.gameHead { + max-width: 700px; + margin-bottom: 32px; +} +.gameLead { + margin: 14px 0 0; + font-size: 16.5px; + line-height: 1.6; + color: var(--b2c-muted); + text-wrap: pretty; +} +.gameCard { + overflow: hidden; + background: var(--b2c-surface); + border: 1px solid var(--b2c-line); + border-radius: 14px; + box-shadow: 0 16px 44px rgba(18, 26, 29, 0.07); +} +.gameGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); +} +.gameMap { + position: relative; + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 24px; + min-height: 300px; + padding: clamp(24px, 3vw, 34px); + background: var(--b2c-surface-dark); +} +.gameMapGrid { + position: absolute; + inset: 0; + opacity: 0.16; + background-image: + linear-gradient(var(--b2c-accent) 1px, transparent 1px), + linear-gradient(90deg, var(--b2c-accent) 1px, transparent 1px); + background-size: 44px 44px; +} +.gameMapRoadH { + position: absolute; + left: 0; + right: 0; + top: 38%; + height: 14px; + background: #2c3a3f; + opacity: 0.7; +} +.gameMapRoadV { + position: absolute; + top: 0; + bottom: 0; + left: 62%; + width: 12px; + background: #2c3a3f; + opacity: 0.7; +} +.gameMapDot { + position: absolute; + width: 16px; + height: 16px; + margin: -8px 0 0 -8px; +} +.gameMapPing { + position: absolute; + inset: 0; + display: block; + border-radius: 50%; + background: var(--b2c-accent); + animation: gamePing 2.4s ease-out infinite; +} +.gameMapCore { + position: absolute; + inset: 0; + display: block; + border-radius: 50%; + background: var(--b2c-accent); + border: 3px solid var(--b2c-surface-dark); +} +@keyframes gamePing { + 0% { transform: scale(1); opacity: 0.55; } + 70%, 100% { transform: scale(2.6); opacity: 0; } +} +@media (prefers-reduced-motion: reduce) { + .gameMapPing { animation: none; } +} +.gameMapLabel { + position: relative; + font-family: var(--b2c-font-mono); + font-size: 9.5px; + letter-spacing: 0.14em; + color: var(--b2c-accent); +} +.gameMapBottom { + position: relative; +} +.gameAddr { + font-weight: 800; + font-size: clamp(19px, 2.2vw, 24px); + line-height: 1.2; + color: #ffffff; +} +.gameMeta { + margin-top: 8px; + font-size: 14px; + color: #9fb0b4; +} +.gameTags { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 16px; +} +.gameTag { + font-family: var(--b2c-font-mono); + font-size: 10.5px; + color: #9fb0b4; + border: 1px solid #2c3a3f; + border-radius: 4px; + padding: 5px 10px; +} +.gamePanel { + display: flex; + flex-direction: column; + justify-content: center; + padding: clamp(24px, 3vw, 34px); +} +.gameFade { + animation: gameFadeIn 0.3s ease; +} +@keyframes gameFadeIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} +@media (prefers-reduced-motion: reduce) { + .gameFade { animation: none; } +} +.gameQuestion { + margin-bottom: 6px; + font-weight: 800; + font-size: clamp(17px, 1.8vw, 20px); + line-height: 1.35; +} +.gameHint { + margin-bottom: 26px; + font-size: 14px; + line-height: 1.55; + color: var(--b2c-muted); +} +.gameGuess { + margin-bottom: 18px; + font-size: clamp(30px, 3.6vw, 40px); + font-weight: 800; + letter-spacing: -0.02em; + line-height: 1; + font-variant-numeric: tabular-nums; +} +.gameRange { + width: 100%; + height: 6px; + margin: 0 0 10px; + appearance: none; + border-radius: 3px; + background: var(--b2c-line); + outline: none; +} +.gameRange::-webkit-slider-thumb { + appearance: none; + width: 26px; + height: 26px; + border-radius: 50%; + background: var(--b2c-accent); + border: 3px solid var(--b2c-surface); + box-shadow: 0 2px 8px rgba(18, 26, 29, 0.25); + cursor: pointer; +} +.gameRange::-moz-range-thumb { + width: 26px; + height: 26px; + border-radius: 50%; + background: var(--b2c-accent); + border: 3px solid var(--b2c-surface); + cursor: pointer; +} +.gameScale { + display: flex; + justify-content: space-between; + margin-bottom: 24px; + font-family: var(--b2c-font-mono); + font-size: 10.5px; + color: #9fb0b4; +} +.gameBtnAccent { + display: block; + width: 100%; + padding: 15px 20px; + border: none; + border-radius: 6px; + background: var(--b2c-accent-deep); + color: var(--b2c-on-accent); + font-family: var(--b2c-font-sans); + font-weight: 700; + font-size: 16px; + text-align: center; + cursor: pointer; + box-sizing: border-box; +} +.gameBtnAccent:hover { + background: var(--b2c-ink); +} +.gameBtnDark { + composes: gameBtnAccent; + background: var(--b2c-surface-dark); +} +.gameBtnDark:hover { + background: var(--b2c-accent-deep); +} +.gameResultEyebrow { + margin-bottom: 16px; + font-family: var(--b2c-font-mono); + font-size: 9.5px; + letter-spacing: 0.14em; + color: var(--b2c-accent-text); +} +.gameRows { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 20px; +} +.gameRowLight { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 12px; + padding: 14px 18px; + border-radius: 8px; + background: var(--b2c-page-bg); +} +.gameRowSoft { + composes: gameRowLight; + background: var(--b2c-surface-soft); +} +.gameRowDark { + composes: gameRowLight; + background: var(--b2c-surface-dark); +} +.gameRowLabel { + font-size: 14px; + color: var(--b2c-muted); +} +.gameRowLabelAccent { + composes: gameRowLabel; + color: var(--b2c-accent-text); +} +.gameRowLabelMuted { + composes: gameRowLabel; + color: #9fb0b4; +} +.gameRowValue { + font-weight: 800; + font-size: 19px; + font-variant-numeric: tabular-nums; +} +.gameRowValueAccent { + composes: gameRowValue; + color: var(--b2c-accent-text); +} +.gameRowValueLight { + composes: gameRowValue; + color: #ffffff; +} +.gameVerdict { + margin-bottom: 22px; + font-size: 15px; + line-height: 1.6; + color: #3a4a4f; +} +.gameFinal { + text-align: center; +} +.gameFinalGrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + margin-bottom: 20px; +} +.gameFinalTileLight { + padding: 20px; + border-radius: 8px; + background: var(--b2c-page-bg); +} +.gameFinalTileSoft { + composes: gameFinalTileLight; + background: var(--b2c-surface-soft); +} +.gameFinalValue { + font-size: 30px; + font-weight: 800; + line-height: 1; + font-variant-numeric: tabular-nums; +} +.gameFinalValueAccent { + composes: gameFinalValue; + color: var(--b2c-accent-text); +} +.gameFinalLabel { + margin-top: 6px; + font-size: 13px; + color: var(--b2c-muted); +} +.gameFinalLabelAccent { + composes: gameFinalLabel; + color: var(--b2c-accent-text); +} +.gameRestart { + margin-top: 14px; + padding: 0; + border: none; + background: none; + color: var(--b2c-muted); + font-family: var(--b2c-font-sans); + font-size: 13px; + font-weight: 600; + cursor: pointer; +} +.gameRestart:hover { + color: var(--b2c-accent-text); +} + +/* --- Два пути ------------------------------------------------------------- */ +.pathsSection { + max-width: 1280px; + margin: 0 auto; + padding: clamp(48px, 6vw, 80px) clamp(20px, 5vw, 56px); +} +.pathsHead { + max-width: 680px; + margin-bottom: 32px; +} +.pathsGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); + gap: 20px; +} +.pathCard { + display: flex; + flex-direction: column; + padding: clamp(24px, 3vw, 34px); + background: var(--b2c-surface); + border: 1px solid var(--b2c-line); + border-radius: 12px; +} +.pathCardDark { + composes: pathCard; + background: var(--b2c-surface-dark); + border-color: var(--b2c-surface-dark); + color: #ffffff; +} +.pathEyebrow { + margin-bottom: 14px; + font-family: var(--b2c-font-mono); + font-size: 10px; + letter-spacing: 0.14em; + color: var(--b2c-muted); +} +.pathEyebrowAccent { + composes: pathEyebrow; + color: var(--b2c-accent); +} +.pathTitle { + margin-bottom: 10px; + font-weight: 800; + font-size: 21px; +} +.pathBody { + flex: 1; + margin: 0; + font-size: 15px; + line-height: 1.6; + color: var(--b2c-muted); +} +.pathBodyDark { + composes: pathBody; + color: #9fb0b4; +} +.pathBtnLight { + display: block; + margin-top: 24px; + padding: 14px 20px; + border: 1px solid var(--b2c-line); + border-radius: 6px; + background: var(--b2c-page-bg); + color: var(--b2c-ink); + font-weight: 700; + font-size: 15px; + text-align: center; +} +.pathBtnLight:hover { + border-color: var(--b2c-accent); + color: var(--b2c-accent-text); +} +.pathBtnAccent { + display: block; + margin-top: 24px; + padding: 14px 20px; + border-radius: 6px; + background: var(--b2c-accent-deep); + color: var(--b2c-on-accent); + font-weight: 700; + font-size: 15px; + text-align: center; +} +.pathBtnAccent:hover { + background: var(--b2c-accent); + color: var(--b2c-on-accent); +} + +/* --- Возражения (FAQ) ----------------------------------------------------- */ +.objSection { + background: var(--b2c-surface); + border-top: 1px solid var(--b2c-line); + border-bottom: 1px solid var(--b2c-line); +} +.objGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); + gap: clamp(28px, 4vw, 56px); + max-width: 1280px; + margin: 0 auto; + padding: clamp(48px, 6vw, 80px) clamp(20px, 5vw, 56px); +} +.objTitle { + margin: 0 0 20px; + font-size: clamp(26px, 3.4vw, 38px); + line-height: 1.12; + letter-spacing: -0.015em; + font-weight: 800; +} +.objCtaBtn { + display: inline-block; + padding: 13px 24px; + border-radius: 6px; + background: var(--b2c-surface-dark); + color: var(--b2c-on-accent); + font-weight: 700; + font-size: 14.5px; +} +.objCtaBtn:hover { + background: var(--b2c-accent-deep); + color: var(--b2c-on-accent); +} +.objList { + display: flex; + flex-direction: column; +} +.objItem { + border-bottom: 1px solid var(--b2c-line); +} +.objSummary { + display: flex; + justify-content: space-between; + align-items: center; + gap: 20px; + padding: 20px 0; + cursor: pointer; + list-style: none; +} +.objSummary::-webkit-details-marker { + display: none; +} +.objQuestion { + font-weight: 800; + font-size: 16.5px; + color: var(--b2c-ink); +} +.objIcon { + position: relative; + flex: none; + width: 16px; + height: 16px; +} +.objIcon::before, +.objIcon::after { + content: ""; + position: absolute; + inset: 50% auto auto 0; + width: 16px; + height: 2px; + margin-top: -1px; + border-radius: 1px; + background: var(--b2c-accent); + transition: transform 0.2s ease; +} +.objIcon::after { + transform: rotate(90deg); +} +.objItem[open] .objIcon::after { + transform: rotate(0deg); +} +.objAnswer { + max-width: 720px; + padding: 0 0 22px; +} +.objAnswer p { + margin: 0 0 10px; + font-size: 15px; + line-height: 1.65; + color: var(--b2c-muted); +} +.objAnswer p:last-child { + margin-bottom: 0; +} + +/* --- Статьи --------------------------------------------------------------- */ +.artSection { + max-width: 1280px; + margin: 0 auto; + padding: clamp(48px, 6vw, 80px) clamp(20px, 5vw, 56px); +} +.artHead { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 24px; + flex-wrap: wrap; + margin-bottom: 28px; +} +.artTitle { + margin: 0; + font-size: clamp(24px, 3vw, 32px); + line-height: 1.15; + letter-spacing: -0.015em; + font-weight: 800; +} +.artGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); + gap: 20px; +} +.artCard { + padding: 26px; + background: var(--b2c-surface); + border: 1px solid var(--b2c-line); + border-radius: 10px; +} +.artRubric { + margin-bottom: 14px; + font-family: var(--b2c-font-mono); + font-size: 9.5px; + letter-spacing: 0.14em; + color: var(--b2c-muted); +} +.artCardTitle { + font-weight: 800; + font-size: 17.5px; + line-height: 1.3; +} diff --git a/tradein-mvp/frontend/src/app/mera-public/marketing-v3.ts b/tradein-mvp/frontend/src/app/mera-public/marketing-v3.ts index 3db0f1df..866c05ab 100644 --- a/tradein-mvp/frontend/src/app/mera-public/marketing-v3.ts +++ b/tradein-mvp/frontend/src/app/mera-public/marketing-v3.ts @@ -164,3 +164,86 @@ export const PROOF_ROWS_PLACEHOLDER: readonly ProofRow[] = [ gainPct: "+3,4%", }, ] as const; + +// --------------------------------------------------------------------------- +// Дополнение по макету 29.08 («B2C модуль для МЕРА», Макс) — секции «лента +// сделок», «игра "Угадай цену"», «возражения», «статьи». Тот же контракт, что +// у величин выше: ничего из этого НЕ подтверждено замером, контура сверки +// «прогноз → факт» в продукте не существует (см. шапку модуля и гейт-тест) — +// поэтому данные живут здесь, под noindex, до появления вычисляемых. +// --------------------------------------------------------------------------- + +/** Раунд мини-игры «Угадай цену»: реквизит из макета, НЕ реальная сделка. */ +export interface GameRound { + readonly district: string; + readonly addr: string; + readonly meta: string; + readonly tags: readonly string[]; + readonly question: string; + readonly hint: string; + /** Слайдер: млн ₽. */ + readonly min: number; + readonly max: number; + readonly step: number; + readonly start: number; + /** «МЕРА сказала» и «факт сделки», млн ₽ — числа из макета. */ + readonly mera: number; + readonly fact: number; + /** Позиция точки на декоративной карте, в процентах. */ + readonly dotX: number; + readonly dotY: number; +} + +export const GAME_ROUNDS_PLACEHOLDER: readonly GameRound[] = [ + { + district: "ЦЕНТРАЛЬНЫЙ Р-Н", + addr: "1-комнатная, 34 м²", + meta: "Центральный р-н · 4/9 этаж · панель", + tags: ["ХОРОШЕЕ СОСТОЯНИЕ", "9 АНАЛОГОВ", "БЛИЗКО К МЕТРО"], + question: "Какую цену поставить, чтобы продать за 30 дней?", + hint: "Продано в марте 2026. Ваш ответ — в миллионах рублей.", + min: 4.5, + max: 7.5, + step: 0.05, + start: 5.4, + mera: 5.55, + fact: 5.5, + dotX: 32, + dotY: 42, + }, + { + district: "ЮГО-ЗАПАД", + addr: "2-комнатная, 52 м²", + meta: "Юго-Западный р-н · 7/16 этаж · монолит", + tags: ["КОСМЕТИКА", "14 АНАЛОГОВ", "ДВОР БЕЗ МАШИН"], + question: "За сколько её реально купили?", + hint: "Продано в мае 2026. Ваш ответ — в миллионах рублей.", + min: 5.5, + max: 9.5, + step: 0.05, + start: 7.2, + mera: 7.4, + fact: 7.55, + dotX: 58, + dotY: 61, + }, + { + district: "УРАЛМАШ", + addr: "3-комнатная, 74 м²", + meta: "Орджоникидзевский р-н · 2/5 этаж · кирпич", + tags: ["ТРЕБУЕТ РЕМОНТА", "6 АНАЛОГОВ", "ТИХИЙ ДВОР"], + question: "Какая цена ушла бы за 45 дней?", + hint: "Продано в июне 2026. Ваш ответ — в миллионах рублей.", + min: 4.0, + max: 8.0, + step: 0.05, + start: 6.0, + mera: 5.85, + fact: 5.95, + dotX: 44, + dotY: 28, + }, +]; + +/** Ошибка МЕРЫ в финале игры — та же величина, что в бейдже героя. */ +export const GAME_MERA_ERROR_LABEL = "1,8%"; diff --git a/tradein-mvp/frontend/src/app/mera-public/v3/page.tsx b/tradein-mvp/frontend/src/app/mera-public/v3/page.tsx index b96fabf3..5a50b038 100644 --- a/tradein-mvp/frontend/src/app/mera-public/v3/page.tsx +++ b/tradein-mvp/frontend/src/app/mera-public/v3/page.tsx @@ -1,12 +1,17 @@ import type { Metadata } from "next"; import { AccuracyV3 } from "../_components/v3/AccuracyV3"; +import { ArticlesV3 } from "../_components/v3/ArticlesV3"; import { CostOfErrorV3 } from "../_components/v3/CostOfErrorV3"; +import { DealsTickerV3 } from "../_components/v3/DealsTickerV3"; import { FooterV3 } from "../_components/v3/FooterV3"; import { HeaderV3 } from "../_components/v3/HeaderV3"; +import { GuessGameV3 } from "../_components/v3/GuessGameV3"; import { HeroV3 } from "../_components/v3/HeroV3"; +import { ObjectionsV3 } from "../_components/v3/ObjectionsV3"; import { StepsV3 } from "../_components/v3/StepsV3"; import { StickyCtaV3 } from "../_components/v3/StickyCtaV3"; +import { TwoPathsV3 } from "../_components/v3/TwoPathsV3"; import styles from "../landing.module.css"; /** @@ -19,15 +24,18 @@ import styles from "../landing.module.css"; * на одном и том же контенте. Переключение — отдельным решением, когда * версия принята. * - * ЧЕГО ЗДЕСЬ ПОКА НЕТ (секции макета, требующие данных или контуров, - * которых нет в коде — каждая заведена отдельной задачей): - * - лента «прогноз → факт» и мини-игра «Проверьте себя» — нужен контур - * сверки прогноза с фактом сделки; - * - «Продажа под ключ» — противоречит текущему утверждению лэндинга - * «мы не берём квартиры на продажу», требует отдельного договора; - * - блок статей — раздела не существует; - * - модалка проверки и рабочая оплата — нет анонимного периметра и - * платёжного контура. + * ОБНОВЛЕНИЕ 29.08 (макет «B2C модуль», Макс): добавлены лента + * «прогноз → факт», игра «Угадай цену», «Два пути», «Возражения» и тизер + * статей. Данные ленты/игры — реквизит из marketing-v3.ts под тем же гейтом + * #2904 (контура сверки прогноза с фактом в продукте по-прежнему нет). + * + * ЧЕГО ЗДЕСЬ ПО-ПРЕЖНЕМУ НЕТ: + * - страницы статей («МЕРА статьи»/«МЕРА статья») — карточки тизера + * рендерятся без ссылок до появления раздела; + * - модалка проверки — сознательно заменена экраном /estimate (см. + * StickyCtaV3), все CTA макетного `openModal` ведут туда; + * - партнёрская заявка «продажи под ключ» — кнопка ведёт в Telegram + * поддержки до появления формы и договора. * * ЧИСЛА в секции «Точность» и «Цена ошибки» — плейсхолдеры из макета, * см. `marketing-v3.ts`: они обязаны быть заменены вычисляемыми до снятия @@ -44,10 +52,15 @@ export default function MeraPublicV3Page() { <>
- + + + + + +