feat(site-finder): НСПД verify deep-links + landing GenDesign vs НСПД (#97 #100) #1314

Merged
bot-backend merged 1 commit from feat/nspd-deeplinks-landing-diff into main 2026-06-13 15:53:03 +00:00
5 changed files with 337 additions and 15 deletions

View file

@ -5,10 +5,12 @@ import { useState } from "react";
import {
BarChart3,
Building2,
Check,
ChevronDown,
ChevronUp,
Database,
MapPin,
Minus,
RefreshCw,
} from "lucide-react";
@ -43,8 +45,97 @@ const FAQ: { q: string; a: string }[] = [
},
];
// ── 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: "Инвест-балл участка и объяснение",
nspd: null,
gendesign: "Score 0100 с разбором факторов",
},
{
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 (
<span
style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
fontSize: 13,
color: "var(--fg-tertiary)",
}}
>
<Minus size={16} strokeWidth={1.5} color="var(--fg-tertiary)" />
нет
</span>
);
}
return (
<span
style={{
display: "inline-flex",
alignItems: "flex-start",
gap: 6,
fontSize: 13,
color: "var(--fg-secondary)",
lineHeight: 1.5,
}}
>
<Check
size={16}
strokeWidth={1.5}
color="var(--success)"
style={{ flexShrink: 0, marginTop: 2 }}
/>
{value}
</span>
);
}
function FaqItem({ q, a }: { q: string; a: string }) {
const [open, setOpen] = useState(false);
return (
@ -149,7 +240,6 @@ export default function HomePage() {
В dev (401) или до загрузки /me UserMenu сам рендерит null. */}
<TopNav rightSlot={<UserMenu />} />
{/* ── Hero ───────────────────────────────────────────────────────── */}
<section
style={{
@ -316,6 +406,101 @@ export default function HomePage() {
</p>
</section>
{/* ── GenDesign vs НСПД (#100) ───────────────────────────────────── */}
<section
style={{
maxWidth: 1280,
margin: "0 auto",
padding: "0 24px 48px",
}}
>
<h2
style={{
margin: "0 0 4px",
fontSize: 18,
fontWeight: 600,
color: "var(--fg-primary)",
}}
>
GenDesign и НСПД
</h2>
<p
style={{
margin: "0 0 20px",
fontSize: 14,
color: "var(--fg-secondary)",
lineHeight: 1.6,
maxWidth: 720,
}}
>
НСПД (nspd.gov.ru) бесплатная государственная карта: кадастр, ПЗЗ,
границы. GenDesign не заменяет её, а считает поверх добавляет рынок,
прогноз и инвест-вердикт. Любую цифру можно сверить с первоисточником
из карточки участка.
</p>
<div
style={{
background: "var(--bg-card)",
border: "1px solid var(--border-card)",
borderRadius: 12,
overflow: "hidden",
maxWidth: 880,
}}
>
{/* Header row */}
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 200px 240px",
gap: 16,
padding: "12px 20px",
borderBottom: "1px solid var(--border-card)",
background: "var(--bg-card-alt)",
}}
>
<div style={comparisonHeadCell}>Возможность</div>
<div style={comparisonHeadCell}>НСПД (бесплатно)</div>
<div style={{ ...comparisonHeadCell, color: "var(--accent)" }}>
GenDesign
</div>
</div>
{/* Data rows */}
{COMPARISON_ROWS.map((row, i) => (
<div
key={row.feature}
style={{
display: "grid",
gridTemplateColumns: "1fr 200px 240px",
gap: 16,
padding: "12px 20px",
alignItems: "start",
borderBottom:
i < COMPARISON_ROWS.length - 1
? "1px solid var(--border-soft)"
: "none",
background:
i % 2 === 0 ? "var(--bg-card)" : "var(--bg-card-alt)",
}}
>
<div
style={{
fontSize: 13,
fontWeight: 500,
color: "var(--fg-primary)",
lineHeight: 1.5,
}}
>
{row.feature}
</div>
<ComparisonCell value={row.nspd} />
<ComparisonCell value={row.gendesign} />
</div>
))}
</div>
</section>
{/* ── Pricing ────────────────────────────────────────────────────── */}
<section
style={{
@ -498,6 +683,22 @@ export default function HomePage() {
>
/health
</a>
<a
href="https://nspd.gov.ru"
target="_blank"
rel="noopener noreferrer"
style={{ color: "var(--accent)", textDecoration: "none" }}
>
НСПД
</a>
<a
href="https://pkk.rosreestr.ru"
target="_blank"
rel="noopener noreferrer"
style={{ color: "var(--accent)", textDecoration: "none" }}
>
ПКК Росреестра
</a>
<span>GenDesign {new Date().getFullYear()}</span>
</footer>
@ -542,3 +743,11 @@ const kpiValue: React.CSSProperties = {
color: "var(--fg-primary)",
fontVariantNumeric: "tabular-nums",
};
const comparisonHeadCell: React.CSSProperties = {
fontSize: 11,
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.04em",
color: "var(--fg-secondary)",
};

View file

@ -0,0 +1,53 @@
"use client";
/**
* NspdVerifyLink tertiary-link «Проверить на НСПД» / «Открыть на ПКК».
*
* Issue #97 кнопка-верификация: открывает официальный бесплатный госсервис
* (НСПД / ПКК Росреестра) по кадастровому номеру в новой вкладке, чтобы
* пользователь мог сверить данные Site Finder с первоисточником одним кликом.
*
* Tertiary CTA (ui-conventions): text-only link с иконкой, токены, без hex.
*/
import { ExternalLink } from "lucide-react";
import { nspdMapUrl, pkkSearchUrl } from "@/lib/nspdLinks";
interface Props {
cadNum: string;
/** Какой источник открывать. nspd — карта-вьюер НСПД, pkk — ПКК Росреестра. */
source?: "nspd" | "pkk";
/** Переопределить подпись (по умолчанию зависит от source). */
label?: string;
}
export function NspdVerifyLink({ cadNum, source = "nspd", label }: Props) {
if (!cadNum) return null;
const href = source === "pkk" ? pkkSearchUrl(cadNum) : nspdMapUrl(cadNum);
const text =
label ?? (source === "pkk" ? "Открыть на ПКК" : "Проверить на НСПД");
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
style={{
display: "inline-flex",
alignItems: "center",
gap: 4,
fontSize: 12,
fontWeight: 500,
color: "var(--accent)",
textDecoration: "none",
whiteSpace: "nowrap",
}}
title={`Сверить данные с официальным источником: ${cadNum}`}
>
<ExternalLink size={14} strokeWidth={1.5} />
{text}
</a>
);
}

View file

@ -17,6 +17,7 @@ import { HeadlineBar } from "@/components/ui/HeadlineBar";
import { KpiCard } from "@/components/site-finder/KpiCard";
import { MiniMap } from "./MiniMap";
import { EgrnPropertyTable } from "./EgrnPropertyTable";
import { NspdVerifyLink } from "./NspdVerifyLink";
import { PoiList2Gis } from "./PoiList2Gis";
import { ExportButtons } from "./ExportButtons";
import {
@ -228,17 +229,32 @@ export function Section1ParcelInfo({ cad }: Props) {
marginBottom: 24,
}}
>
{/* Section title */}
<h2
{/* Section title + verify-links на официальные источники (#97) */}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 12,
flexWrap: "wrap",
margin: "0 0 12px",
fontSize: 18,
fontWeight: 600,
color: "var(--fg-primary)",
}}
>
1. Объект
</h2>
<h2
style={{
margin: 0,
fontSize: 18,
fontWeight: 600,
color: "var(--fg-primary)",
}}
>
1. Объект
</h2>
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
<NspdVerifyLink cadNum={cad} source="nspd" />
<NspdVerifyLink cadNum={cad} source="pkk" />
</div>
</div>
{/* HeadlineBar — verdict */}
<div style={{ marginBottom: 16 }}>

View file

@ -23,6 +23,7 @@ import {
Info,
} from "lucide-react";
import { HeadlineBar } from "@/components/ui/HeadlineBar";
import { NspdVerifyLink } from "./NspdVerifyLink";
import { useParcelAnalyzeQuery } from "@/lib/site-finder-api";
import type { UtilitySummaryItem } from "@/lib/site-finder-api";
@ -487,17 +488,33 @@ export function Section2NetworksUtilities({ cad }: Props) {
marginBottom: 24,
}}
>
{/* Section title */}
<h2
{/* Section title + verify-link на НСПД (#97) */}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 12,
flexWrap: "wrap",
margin: "0 0 12px",
fontSize: 18,
fontWeight: 600,
color: "var(--fg-primary)",
}}
>
2. Сети и точки подключения
</h2>
<h2
style={{
margin: 0,
fontSize: 18,
fontWeight: 600,
color: "var(--fg-primary)",
}}
>
2. Сети и точки подключения
</h2>
<NspdVerifyLink
cadNum={cad}
source="nspd"
label="Проверить сети на НСПД"
/>
</div>
{/* HeadlineBar */}
<div style={{ marginBottom: 16 }}>

View file

@ -0,0 +1,27 @@
/**
* Deep-links на официальные государственные источники (НСПД, ПКК Росреестра).
*
* Issue #97 пользователь должен мочь верифицировать любые данные Site Finder
* на бесплатном официальном сервисе одним кликом. Единая точка построения URL,
* чтобы не дублировать литералы `nspd.gov.ru` / `pkk.rosreestr.ru` по компонентам
* и иметь один безопасный канал кодирования кадастрового номера.
*
* Существующие потребители паттерна (до централизации): NspdZoningBlock,
* NspdOpportunityBlock, ParcelDrawer, AnalysisSidebar.
*/
/**
* НСПД (Национальная система пространственных данных) карта-вьюер по
* кадастровому номеру. Базовый сервис «посмотреть участок на госкарте».
*/
export function nspdMapUrl(cadNum: string): string {
return `https://nspd.gov.ru/map?cadastralNumber=${encodeURIComponent(cadNum)}`;
}
/**
* Публичная кадастровая карта Росреестра (ПКК) поиск по кадастровому номеру.
* Используется как fallback / cross-check к НСПД.
*/
export function pkkSearchUrl(cadNum: string): string {
return `https://pkk.rosreestr.ru/#/search/${encodeURIComponent(cadNum)}`;
}