"use client"; import { useState } from "react"; import { BarChart3, Building2, Check, ChevronDown, ChevronUp, Database, MapPin, Minus, RefreshCw, } from "lucide-react"; import { UserMenu } from "@/components/auth/UserMenu"; import { PilotRequestModal } from "@/components/landing/PilotRequestModal"; import { TopNav } from "@/components/landing/TopNav"; import { HeadlineBar } from "@/components/ui/HeadlineBar"; import { useLandingStats } from "@/lib/api/landing"; // ── FAQ data ─────────────────────────────────────────────────────────────── const FAQ: { q: string; a: string }[] = [ { q: "Откуда берутся данные?", a: "Три источника: Росреестр (ДДУ-сделки, >7 млн записей), DOM.РФ (портфель новостроек, ПЗЗ), Objective (цены предложения). Обновление еженедельное для DOM.РФ и цен, ежеквартальное для Росреестра.", }, { q: "Насколько точны цены?", a: "Цены предложения актуальны на дату последнего обновления. Цена сделки из ДДУ — реальная стоимость на момент регистрации в Росреестре. Расхождение с рыночной ценой в пике может составлять 5–15%.", }, { q: "Какие районы и регионы охвачены?", a: "Сейчас — Екатеринбург и Свердловская область (ПЗЗ ЕКБ, МСК-66). Расширение на другие регионы УрФО в роудмапе.", }, { q: "Что входит в пилотный доступ?", a: "Полный доступ к Site Finder (анализ участков по кадастровому номеру), аналитике рынка Свердловской области и рекомендатору квартирографии. Срок пилота — 30 дней. Поддержка по email.", }, { q: "Как устроен маппинг Objective → DOM.РФ?", a: "Объекты Objective сопоставляются с портфелем DOM.РФ по нечёткому совпадению названия и геокоординатам. Текущее покрытие — около 11% (один DOM.РФ-объект может объединять несколько Objective-лотов).", }, ]; // ── GenDesign vs НСПД — value-prop differentiation (#100) ──────────────────── // НСПД (nspd.gov.ru) — бесплатная госкарта пространственных данных. GenDesign // не заменяет её, а считает поверх: рынок, прогноз, инвест-вердикт. Таблица // объясняет, за что девелопер платит сверх бесплатного источника. interface ComparisonRow { feature: string; /** null = нет совсем; string = есть, но в каком виде. */ nspd: string | null; gendesign: string; } const COMPARISON_ROWS: ComparisonRow[] = [ { feature: "Кадастр, ПЗЗ, ЗОУИТ, границы участка", nspd: "Карта и атрибуты", gendesign: "То же + сверка одним кликом", }, { feature: "Цены сделок ДДУ (Росреестр)", nspd: null, gendesign: "Медиана по району и классу", }, { feature: "Остатки и скорость продаж новостроек", nspd: null, gendesign: "DOM.РФ + Objective, еженедельно", }, { feature: "POI-скор участка и объяснение", nspd: null, gendesign: "POI-скор 0–40 с разбором факторов", }, { feature: "Прогноз цены и горизонты выхода", nspd: null, gendesign: "Прогноз с доверительным интервалом", }, { feature: "Рекомендация квартирографии", nspd: null, gendesign: "Mix планировок под спрос района", }, { feature: "Экспорт в PDF / Excel / DXF", nspd: null, gendesign: "Отчёт по участку в один клик", }, ]; // ── Sub-components ───────────────────────────────────────────────────────── function ComparisonCell({ value }: { value: string | null }) { if (value === null) { return ( нет ); } return ( {value} ); } function FaqItem({ q, a }: { q: string; a: string }) { const [open, setOpen] = useState(false); return (
{open ? (

{a}

) : null}
); } // ── KPI formatting ───────────────────────────────────────────────────────── function formatNumber(n: number | undefined, decimals = 0): string { if (n === undefined) return "—"; return n.toLocaleString("ru", { minimumFractionDigits: decimals, maximumFractionDigits: decimals, }); } /** Skeleton placeholder shown when stale=true (no fake data) */ function KpiSkeleton() { return (
); } // ── Main component ───────────────────────────────────────────────────────── export default function HomePage() { const [modalOpen, setModalOpen] = useState(false); const { data: stats, isLoading } = useLandingStats(); const isStale = stats?.stale === true; const showSkeleton = isLoading || isStale; return (
{/* ── Nav ────────────────────────────────────────────────────────── */} {/* UserMenu — личный кабинет (avatar + dropdown) для залогиненного юзера. В dev (401) или до загрузки /me — UserMenu сам рендерит null. */} } /> {/* ── Hero ───────────────────────────────────────────────────────── */}

Цифры рынка перед фундаментом

Аналитика новостроек Свердловской области — Росреестр, DOM.РФ, Objective — в одном инструменте для девелоперов.

{/* ── KPI cards ──────────────────────────────────────────────── */}
{/* zk_total */}
ЖК DOM.РФ в ЕКБ
{showSkeleton ? : formatNumber(stats?.zk_total)}
{/* deals_total */}
ДДУ-сделки Росреестра
{showSkeleton ? ( ) : ( formatNumber(stats?.deals_total) )}
{/* price_coverage_pct */}
Покрытие ценами Objective
{showSkeleton ? ( ) : ( `${formatNumber(stats?.price_coverage_pct, 1)}%` )}
{/* mapping_coverage_pct */}
Маппинг Objective → DOM.РФ
{showSkeleton ? ( ) : ( `${formatNumber(stats?.mapping_coverage_pct, 1)}%` )}
{/* last_data_update */}
Последнее обновление данных
{showSkeleton ? ( ) : ( (stats?.last_data_update ?? "—") )}
{isStale && (

Данные обновляются...

)}
{/* ── Парадокс портфеля ──────────────────────────────────────────── */}

DOM.РФ фиксирует строящиеся ЖК, но цены предложения Objective покрывают лишь часть из них. Из тех, где цены есть, публично доступны единицы — остальное требует прямого запроса или данных, агрегированных GenDesign.

{/* ── GenDesign vs НСПД (#100) ───────────────────────────────────── */}

GenDesign и НСПД

НСПД (nspd.gov.ru) — бесплатная государственная карта: кадастр, ПЗЗ, границы. GenDesign не заменяет её, а считает поверх — добавляет рынок, прогноз и инвест-вердикт. Любую цифру можно сверить с первоисточником из карточки участка.

{/* Header row */}
Возможность
НСПД (бесплатно)
GenDesign
{/* Data rows */} {COMPARISON_ROWS.map((row, i) => (
{row.feature}
))}
{/* ── Pricing ────────────────────────────────────────────────────── */}
Пилотная подписка
30 000 ₽ / мес
    {[ "Site Finder — анализ участков по кадастру", "Аналитика рынка Свердловской области", "Рекомендатор квартирографии", "Данные DOM.РФ, Росреестр, Objective", "Email-поддержка в рабочие дни", ].map((item) => (
  • {item}
  • ))}
{/* ── FAQ ────────────────────────────────────────────────────────── */}

Частые вопросы

Об источниках данных, точности и поддержке.

{FAQ.map((item) => ( ))}
{/* ── Footer ─────────────────────────────────────────────────────── */} {/* ── Modal ──────────────────────────────────────────────────────── */} setModalOpen(false)} />
); } // ── Shared styles ────────────────────────────────────────────────────────── const kpiCardStyle: React.CSSProperties = { background: "var(--bg-card)", border: "1px solid var(--border-card)", borderRadius: 12, padding: "16px 18px", }; const kpiIconWrap: React.CSSProperties = { width: 28, height: 28, background: "var(--accent-soft)", borderRadius: 6, display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8, }; const kpiLabel: React.CSSProperties = { fontSize: 12, textTransform: "uppercase", letterSpacing: "0.04em", color: "var(--fg-secondary)", fontWeight: 500, marginBottom: 6, }; const kpiValue: React.CSSProperties = { fontSize: 28, fontWeight: 600, color: "var(--fg-primary)", fontVariantNumeric: "tabular-nums", }; const comparisonHeadCell: React.CSSProperties = { fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.04em", color: "var(--fg-secondary)", };