diff --git a/tradein-mvp/frontend/src/app/mera-public/_components/v3/CityPicker.tsx b/tradein-mvp/frontend/src/app/mera-public/_components/v3/CityPicker.tsx deleted file mode 100644 index 7ef9ee8d..00000000 --- a/tradein-mvp/frontend/src/app/mera-public/_components/v3/CityPicker.tsx +++ /dev/null @@ -1,140 +0,0 @@ -"use client"; - -/** - * CityPicker — выбор города в шапке лэндинга v3 (макет ~ строки 47-73). - * - * Презентационный клиентский остров: сегодня публичный расчёт выключен - * (`PUBLIC_ESTIMATE_ENABLED = false` в content.ts), поэтому выбранный здесь - * город никуда не отправляется — состояние живёт только в этом компоненте и - * показывает посетителю, что сервис знает больше одного города области. - * Когда форма расчёта откроется, сюда нужно будет добавить проброс выбора - * наружу (проп/контекст) — в этом заходе scope ограничен шапкой/подвалом/ - * sticky-баром, самой формы здесь нет. - * - * Список городов и деление на «полное покрытие» / «данных меньше» берутся из - * `OBLAST_CITIES` / `PRIMARY_CITY` (content.ts, тот же реестр, что честно - * объясняет разницу в покрытии в Hero.tsx). Макет показывал числовые «веса» - * по городам (тысячи объявлений) — мы их не повторяем: таких цифр в коде нет, - * а гадать нельзя (правило честности в content.ts). - */ - -import { useEffect, useId, useRef, useState } from "react"; - -import { DEFAULT_CITY, OBLAST_CITIES } from "@/lib/city-registry"; - -import { PRIMARY_CITY, REGION_NAME } from "../../content"; -import styles from "../../landing-v3.module.css"; - -export function CityPicker() { - const [open, setOpen] = useState(false); - const [query, setQuery] = useState(""); - const [city, setCity] = useState(DEFAULT_CITY.label); - const wrapRef = useRef(null); - const buttonRef = useRef(null); - const panelId = useId(); - - // Клик вне панели — закрыть. Тот же паттерн, что components/auth/UserMenu.tsx. - useEffect(() => { - if (!open) return; - function handleClick(e: MouseEvent) { - if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) { - setOpen(false); - } - } - document.addEventListener("mousedown", handleClick); - return () => document.removeEventListener("mousedown", handleClick); - }, [open]); - - // Escape — закрыть и вернуть фокус на кнопку. - useEffect(() => { - if (!open) return; - function handleKey(e: KeyboardEvent) { - if (e.key === "Escape") { - setOpen(false); - buttonRef.current?.focus(); - } - } - document.addEventListener("keydown", handleKey); - return () => document.removeEventListener("keydown", handleKey); - }, [open]); - - const normalizedQuery = query.trim().toLowerCase(); - const filtered = normalizedQuery - ? OBLAST_CITIES.filter((c) => - c.label.toLowerCase().includes(normalizedQuery), - ) - : OBLAST_CITIES; - - return ( -
- - - {open ? ( -
-
- setQuery(e.target.value)} - placeholder="Поиск города" - aria-label="Поиск города" - /> -
-
- {filtered.length > 0 ? ( - filtered.map((c) => ( - - )) - ) : ( -

- Города нет в списке. Мы работаем там, где хватает данных по - сделкам — напишите нам, и добавим. -

- )} -
-
- ДАННЫЕ ПО {OBLAST_CITIES.length} ГОРОДАМ:{" "} - {REGION_NAME.toUpperCase()} -
-
- ) : null} -
- ); -} diff --git a/tradein-mvp/frontend/src/app/mera-public/_components/v3/FreeCheckCard.tsx b/tradein-mvp/frontend/src/app/mera-public/_components/v3/FreeCheckCard.tsx index bd045ed1..5b8a4dee 100644 --- a/tradein-mvp/frontend/src/app/mera-public/_components/v3/FreeCheckCard.tsx +++ b/tradein-mvp/frontend/src/app/mera-public/_components/v3/FreeCheckCard.tsx @@ -28,10 +28,11 @@ * Правило из шапки `lib/city-registry.ts`: город считается известным, только * если человек выбрал его сам ИЛИ `detectCityInText` нашёл его в тексте * адреса; молча подставлять Екатеринбург нельзя. Ответ про город вне покрытия - * — тот же и дословно тот же, что в v1. Выпадашка города в шапке - * (`CityPicker`) сюда НЕ пробрасывается: она презентационная и своим выбором - * не владеет (см. её JSDoc), а гейт обязан читать то, что человек указал - * рядом с адресом. + * — тот же и дословно тот же, что в v1. Выпадашки города в шапке больше нет + * вовсе (`CityPicker` удалён 29.08.2026, см. JSDoc HeaderV3): она была + * презентационной и своим выбором не владела, то есть значение задавалось в + * двух местах, а на расчёт влияло только это. Гейт обязан читать то, что + * человек указал рядом с адресом, — и теперь это единственное такое место. * * СОСТОЯНИЕ ТЕПЕРЬ ДОЕЗЖАЕТ ДО ЧЕРНОВИКА. Поле было обязательным, а его * значение никуда не клалось — человек выбирал состояние квартиры, и мы его diff --git a/tradein-mvp/frontend/src/app/mera-public/_components/v3/HeaderV3.tsx b/tradein-mvp/frontend/src/app/mera-public/_components/v3/HeaderV3.tsx index e97ada9f..88d71e80 100644 --- a/tradein-mvp/frontend/src/app/mera-public/_components/v3/HeaderV3.tsx +++ b/tradein-mvp/frontend/src/app/mera-public/_components/v3/HeaderV3.tsx @@ -1,10 +1,34 @@ /** * HeaderV3 — шапка лэндинга v3 (макет ~ строки 32-78): sticky, blur, лого + - * навигация + селектор города + CTA. + * навигация + CTA. * * Логотип НЕ ссылка: на публичном домене корень — это сам лэндинг, то есть * ссылка вела бы на текущую же страницу. * + * СЕЛЕКТОРА ГОРОДА ЗДЕСЬ БОЛЬШЕ НЕТ (29.08.2026). `CityPicker` жил в этой + * шапке с тех пор, когда карточка проверки города не спрашивала вовсе, и + * своим выбором он НЕ владел: состояние оставалось внутри компонента и никуда + * не отправлялось (см. его прежний JSDoc). Город у формы появился — + * `FreeCheckCard` спрашивает его сама и на нём стоит гео-гейт расчёта (#2576), + * — и с этого момента шапка стала вторым местом, задающим одно значение, но + * ни на что не влияющим: человек выбирал в шапке Нижний Тагил и всё равно + * заполнял город в форме. Правило то же, по которому селектора нет во + * внутренней шапке (`_components/InnerHeader.tsx`). Граница покрытия, ради + * которой выпадашку когда-то читали, теперь стоит текстом в герое (плашка + * «полное покрытие / данных меньше», HeroV3), то есть видна без клика. + * + * НА УЗКОМ ЭКРАНЕ ШАПКА — ОДНА СТРОКА: лого и «Для бизнеса». Навигация по + * секциям и CTA скрыты в CSS (`landing-v3.module.css`, блок шапки) и + * возвращаются с той ширины, на которой шапка макета замерена как + * однострочная, — а не с общего для файла шага в 720px, где она всё ещё + * переносится. На 375px переносилось всё, и липкая шапка занимала 29% + * экрана — эти проценты отъедались при любой прокрутке. Куда делись пункты: «Точность» + * и «Проверьте себя» — секции этой же страницы, до них доезжают прокруткой + * (ради чего на мобильном и скроллят); «Статьи» и «МЕРА для бизнеса» стоят + * ссылками в подвале (`FooterV3`); «Продажа под ключ» и на десктопе не + * ссылка, а заглушка. CTA не потерян: снизу висит `StickyCtaV3` — на экране + * остаётся ОДИН призыв вместо двух одинаковых. + * * НЕАКТИВНЫЕ ПУНКТЫ (29.08.2026). Осталась одна «Продажа под ключ» — * партнёрской формы нет ни в коде, ни у владельца, и пункт остаётся видимым, * но некликабельным. «Проверьте себя» и «Статьи» раньше стояли рядом с ней по @@ -21,16 +45,14 @@ * «Для бизнеса» ведёт на АБСОЛЮТНЫЙ адрес B2B-контура: относительный «/» на * публичном домене вёл на сам лэндинг, то есть пункт не работал вовсе. * - * Единственный клиентский остров — `CityPicker` (открытие/закрытие, поиск). - * Сама шапка серверная. + * Шапка целиком серверная: клиентских островов в ней не осталось (последним + * был `CityPicker`). */ import { B2B_URL } from "../../content"; import styles from "../../landing-v3.module.css"; import { PublicLink } from "../PublicLink"; -import { CityPicker } from "./CityPicker"; - export function HeaderV3() { return (
@@ -65,7 +87,6 @@ export function HeaderV3() {
- Для бизнеса diff --git a/tradein-mvp/frontend/src/app/mera-public/_components/v3/StickyCtaV3.tsx b/tradein-mvp/frontend/src/app/mera-public/_components/v3/StickyCtaV3.tsx index 0b95e7c4..21f1be7d 100644 --- a/tradein-mvp/frontend/src/app/mera-public/_components/v3/StickyCtaV3.tsx +++ b/tradein-mvp/frontend/src/app/mera-public/_components/v3/StickyCtaV3.tsx @@ -15,6 +15,16 @@ * того, куда именно в дерево страницы его вставят (ожидаемое место — сразу * после подвала, как Header/Footer в текущем `layout.tsx`). * + * НА УЗКОМ ЭКРАНЕ ПАНЕЛЬ — ТОЛЬКО КНОПКА (29.08.2026). Строка «Узнайте цену + * своей квартиры…» на 375px переносилась и панель занимала 13% экрана, а + * вместе с шапкой хром съедал 42% при любой прокрутке. Текст скрыт в CSS + * (`.barText`), а не удалён: с той же ширины, что возвращает шапку макета + * (1000px — там строка и кнопка встают рядом), панель снова целиком. То же + * обещание на самой странице остаётся словами героя («Бесплатно: сколько + * похожих квартир продаётся рядом и как давно они висят»), то есть уходит + * повтор, а не мысль. Призыв на мобильном теперь ОДИН: `HeaderV3` свою + * кнопку на этой ширине не показывает. + * * НА КНОПКЕ НЕТ СРОКА. Здесь стояло «Проверить за 30 секунд» — замера времени * заполнения формы нет и не было (та же причина, по которой «30 СЕКУНД» ушло * из бейджа первого шага, см. `landing-facts.ts`). Панель висит на экране 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 a9f09a78..c3705e01 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 @@ -17,14 +17,15 @@ * * ------------------------------------------------------------------------ * ЭТОТ БЛОК (шапка / подвал / sticky-CTA) — префиксы hdr / ftr / bar. - * Секция «Шапка» = HeaderV3.tsx + CityPicker.tsx. + * Секция «Шапка» = HeaderV3.tsx (селектор города из неё убран 29.08.2026: + * город спрашивает сама форма проверки, FreeCheckCard). * Секция «Подвал» = FooterV3.tsx. * Секция «Sticky CTA» = StickyCtaV3.tsx. * ------------------------------------------------------------------------ */ /* ========================================================================== - * Шапка (HeaderV3 + CityPicker) + * Шапка (HeaderV3) * ========================================================================== */ .hdrRoot { @@ -40,11 +41,15 @@ border-bottom: 1px solid var(--b2c-line); } +/* База — узкий экран: в шапке остаются только лого и «Для бизнеса», они + помещаются в одну строку, поэтому и высота узкая (`flex-wrap` тут страховка, + а не режим работы). Навигация и CTA возвращаются в @media-блоке в конце + секции. */ .hdrInner { max-width: 1280px; margin-inline: auto; - padding: 10px clamp(20px, 5vw, 56px); - min-height: 64px; + padding: 8px clamp(20px, 5vw, 56px); + min-height: 52px; display: flex; align-items: center; gap: 14px 26px; @@ -101,7 +106,10 @@ } .hdrNav { - display: flex; + /* База — узкий экран, где навигации по секциям НЕТ: четыре пункта + переносились в две строки и вместе с остальным делали липкую шапку в 29% + экрана. Куда делись пункты — в шапке HeaderV3.tsx. */ + display: none; gap: 10px 22px; flex-wrap: wrap; font-size: 14.5px; @@ -119,8 +127,9 @@ color: var(--b2c-accent-text); } -/* «Статьи» — раздела нет в коде (ни маршрута, ни контента): пункт нарочно - неинтерактивен (aria-disabled, без href), а не ссылка в 404. */ +/* «Продажа под ключ» — раздела нет в коде (ни маршрута, ни контента): пункт + нарочно неинтерактивен (aria-disabled, без href), а не ссылка в 404. + «Статьи» стояли здесь же, пока не появился `articles/`. */ .hdrNavDisabled { color: var(--b2c-muted); opacity: 0.65; @@ -164,7 +173,11 @@ color: var(--b2c-on-accent); } +/* На узком экране призыв в шапке скрыт: он вёл туда же, куда липкая нижняя + панель (`.barRoot`), и два одинаковых призыва на одном экране — это ровно то + «много всплывашек», на которое жалуются. Панель остаётся, шапка отдаёт. */ .hdrCta { + display: none; padding: 11px 22px; font-size: 14px; } @@ -174,146 +187,24 @@ filter: brightness(0.92); } -/* -- Селектор города (CityPicker) ---------------------------------------- */ +/* Шапка макета целиком — с той ширины, на которой она РЕАЛЬНО влезает в одну + строку (замерено: 1000px — лого, четыре пункта, «Для бизнеса» и кнопка; + на 720px то же самое переносится в две строки и даёт 109px липкого хрома, + то есть ровно тот дефект, от которого узкий экран и лечили). Отсюда шаг + 1000, а не общий для файла 720. */ +@media (min-width: 1000px) { + .hdrInner { + padding-block: 10px; + min-height: 64px; + } -.hdrCityWrap { - position: relative; -} + .hdrNav { + display: flex; + } -.hdrCityBtn { - display: flex; - align-items: center; - gap: 8px; - background: none; - border: 1px solid var(--b2c-line); - border-radius: 6px; - padding: 9px 14px; - cursor: pointer; - font-family: var(--b2c-font-sans); - font-size: 14px; - font-weight: 700; - color: var(--b2c-ink); -} - -.hdrCityBtn:hover { - border-color: var(--b2c-accent); - color: var(--b2c-accent-text); -} - -.hdrCityDot { - width: 6px; - height: 6px; - flex: 0 0 auto; - display: block; - border-radius: 50%; - background: var(--b2c-accent); -} - -.hdrCityCaret { - font-family: var(--b2c-font-mono); - font-size: 11px; - color: var(--b2c-muted); -} - -.hdrCityPanel { - position: absolute; - top: calc(100% + 10px); - left: 0; - z-index: 80; - width: min(84vw, 340px); - max-width: calc(100vw - 40px); - overflow: hidden; - background: var(--b2c-surface); - border: 1px solid var(--b2c-line); - border-radius: 10px; - box-shadow: 0 24px 64px color-mix(in srgb, var(--b2c-ink) 18%, transparent); -} - -.hdrCitySearchRow { - padding: 14px 16px; - border-bottom: 1px solid var(--b2c-line-soft); -} - -.hdrCityInput { - width: 100%; - box-sizing: border-box; - border: 1px solid var(--b2c-line); - border-radius: 6px; - padding: 11px 14px; - font-family: var(--b2c-font-sans); - font-size: 14.5px; - color: var(--b2c-ink); - background: var(--b2c-page-bg); - outline: none; -} - -.hdrCityInput:focus { - border-color: var(--b2c-accent); - background: var(--b2c-surface); -} - -.hdrCityList { - max-height: 300px; - overflow-y: auto; - padding: 6px 0; -} - -.hdrCityItem { - width: 100%; - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - background: none; - border: none; - padding: 11px 18px; - text-align: left; - cursor: pointer; - font-family: var(--b2c-font-sans); -} - -.hdrCityItem:hover { - background: var(--b2c-page-bg); -} - -/* Текущий выбранный город (проставляется из CityPicker.tsx как data-атрибут, - не класс — состояние живёт в JS, а не в CSS-модуле). */ -.hdrCityItem[data-active="true"] { - background: var(--b2c-surface-soft); -} - -.hdrCityItem[data-active="true"] .hdrCityItemName { - font-weight: 800; -} - -.hdrCityItemName { - font-size: 14.5px; - font-weight: 600; - color: var(--b2c-ink); -} - -.hdrCityItemNote { - font-family: var(--b2c-font-mono); - font-size: 10.5px; - color: var(--b2c-muted); - white-space: nowrap; -} - -.hdrCityEmpty { - margin: 0; - padding: 22px 18px; - font-size: 14px; - line-height: 1.55; - color: var(--b2c-muted); -} - -.hdrCityFooter { - padding: 12px 18px; - border-top: 1px solid var(--b2c-line-soft); - font-family: var(--b2c-font-mono); - font-size: 10px; - letter-spacing: 0.08em; - color: var(--b2c-muted); + .hdrCta { + display: inline-flex; + } } /* ========================================================================== @@ -472,16 +363,10 @@ /* Компенсация fixed-позиционирования `.barRoot` (тот вырезан из потока документа): без спейсера панель легла бы поверх последних пикселей страницы при докрутке до конца (реквизиты в подвале, последний вопрос - FAQ). Высота — на мобильном тексте+кнопка чаще переносятся на две строки - (flex-wrap), поэтому база выше; на 720px+ строка одна. */ + FAQ). Высота одна на все ширины: панель везде в одну строку — на узком + экране за счёт того, что текст скрыт (см. `.barText`), а не переносится. */ .barSpacer { - height: 96px; -} - -@media (min-width: 720px) { - .barSpacer { - height: 68px; - } + height: 68px; } .barRoot { @@ -495,14 +380,20 @@ justify-content: center; gap: 16px 24px; flex-wrap: wrap; - padding: 12px clamp(16px, 4vw, 32px); + padding: 10px clamp(16px, 4vw, 32px); /* Фоллбэк на непрозрачный surfaceDark для браузеров без color-mix(). */ background: var(--b2c-surface-dark); background: color-mix(in srgb, var(--b2c-surface-dark) 97%, transparent); backdrop-filter: blur(8px); } +/* На узком экране панель — только кнопка. Строка «Узнайте цену своей + квартиры…» переносилась на две-три строки и делала постоянную панель втрое + выше кнопки; то же самое сказано словами в герое (заголовок, лид и перк + «Бесплатно: сколько похожих квартир продаётся рядом»), то есть со страницы + мысль не пропадает — пропадает её третий повтор поверх контента. */ .barText { + display: none; margin: 0; text-align: center; font-weight: 700; @@ -518,6 +409,18 @@ font-size: 14.5px; } +/* Панель макета целиком — с той же ширины, что и шапка (1000px): ниже неё + текст и кнопка тоже переносились, и панель становилась вдвое выше. */ +@media (min-width: 1000px) { + .barRoot { + padding-block: 12px; + } + + .barText { + display: block; + } +} + /* ========================================================================== * HERO (hero*) — первый экран: герой-текст + карточка «Бесплатная проверка». * Секция «Герой» = HeroV3.tsx + FreeCheckCard.tsx + FreeResultV3.tsx.