feat(site-finder): НСПД verify deep-links + landing GenDesign vs НСПД таблица (#97 #100)
All checks were successful
CI / changes (push) Successful in 6s
CI / backend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (push) Successful in 54s
CI / frontend-tests (pull_request) Successful in 57s
All checks were successful
CI / changes (push) Successful in 6s
CI / backend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (push) Successful in 54s
CI / frontend-tests (pull_request) Successful in 57s
#97: nspdLinks helper (nspdMapUrl/pkkSearchUrl, encodeURIComponent) + NspdVerifyLink компонент (var(--accent), rel=noopener) → одноклик-сверка с НСПД/ПКК в Section1 (кадастр/ЕГРН) + Section2 (инж.сети) карточках analyze. #100: landing — сравнительная секция «GenDesign и НСПД» (7-row таблица value-prop: цены ДДУ/остатки/скорость/инвест-балл/прогноз/квартирография/экспорт сверх бесплатной гос-карты) + footer deep-links. Closes #97 Closes #100
This commit is contained in:
parent
84d08940c6
commit
35871a32bd
5 changed files with 337 additions and 15 deletions
|
|
@ -5,10 +5,12 @@ import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
BarChart3,
|
BarChart3,
|
||||||
Building2,
|
Building2,
|
||||||
|
Check,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Database,
|
Database,
|
||||||
MapPin,
|
MapPin,
|
||||||
|
Minus,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
} from "lucide-react";
|
} 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 0–100 с разбором факторов",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: "Прогноз цены и горизонты выхода",
|
||||||
|
nspd: null,
|
||||||
|
gendesign: "Прогноз с доверительным интервалом",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: "Рекомендация квартирографии",
|
||||||
|
nspd: null,
|
||||||
|
gendesign: "Mix планировок под спрос района",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: "Экспорт в PDF / Excel / DXF",
|
||||||
|
nspd: null,
|
||||||
|
gendesign: "Отчёт по участку в один клик",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// ── Sub-components ─────────────────────────────────────────────────────────
|
// ── 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 }) {
|
function FaqItem({ q, a }: { q: string; a: string }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
return (
|
return (
|
||||||
|
|
@ -149,7 +240,6 @@ export default function HomePage() {
|
||||||
В dev (401) или до загрузки /me — UserMenu сам рендерит null. */}
|
В dev (401) или до загрузки /me — UserMenu сам рендерит null. */}
|
||||||
<TopNav rightSlot={<UserMenu />} />
|
<TopNav rightSlot={<UserMenu />} />
|
||||||
|
|
||||||
|
|
||||||
{/* ── Hero ───────────────────────────────────────────────────────── */}
|
{/* ── Hero ───────────────────────────────────────────────────────── */}
|
||||||
<section
|
<section
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -316,6 +406,101 @@ export default function HomePage() {
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</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 ────────────────────────────────────────────────────── */}
|
{/* ── Pricing ────────────────────────────────────────────────────── */}
|
||||||
<section
|
<section
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -498,6 +683,22 @@ export default function HomePage() {
|
||||||
>
|
>
|
||||||
/health
|
/health
|
||||||
</a>
|
</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>
|
<span>GenDesign {new Date().getFullYear()}</span>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|
@ -542,3 +743,11 @@ const kpiValue: React.CSSProperties = {
|
||||||
color: "var(--fg-primary)",
|
color: "var(--fg-primary)",
|
||||||
fontVariantNumeric: "tabular-nums",
|
fontVariantNumeric: "tabular-nums",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const comparisonHeadCell: React.CSSProperties = {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 600,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: "0.04em",
|
||||||
|
color: "var(--fg-secondary)",
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@ import { HeadlineBar } from "@/components/ui/HeadlineBar";
|
||||||
import { KpiCard } from "@/components/site-finder/KpiCard";
|
import { KpiCard } from "@/components/site-finder/KpiCard";
|
||||||
import { MiniMap } from "./MiniMap";
|
import { MiniMap } from "./MiniMap";
|
||||||
import { EgrnPropertyTable } from "./EgrnPropertyTable";
|
import { EgrnPropertyTable } from "./EgrnPropertyTable";
|
||||||
|
import { NspdVerifyLink } from "./NspdVerifyLink";
|
||||||
import { PoiList2Gis } from "./PoiList2Gis";
|
import { PoiList2Gis } from "./PoiList2Gis";
|
||||||
import { ExportButtons } from "./ExportButtons";
|
import { ExportButtons } from "./ExportButtons";
|
||||||
import {
|
import {
|
||||||
|
|
@ -228,17 +229,32 @@ export function Section1ParcelInfo({ cad }: Props) {
|
||||||
marginBottom: 24,
|
marginBottom: 24,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Section title */}
|
{/* Section title + verify-links на официальные источники (#97) */}
|
||||||
<h2
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: 12,
|
||||||
|
flexWrap: "wrap",
|
||||||
margin: "0 0 12px",
|
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 */}
|
{/* HeadlineBar — verdict */}
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import {
|
||||||
Info,
|
Info,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { HeadlineBar } from "@/components/ui/HeadlineBar";
|
import { HeadlineBar } from "@/components/ui/HeadlineBar";
|
||||||
|
import { NspdVerifyLink } from "./NspdVerifyLink";
|
||||||
import { useParcelAnalyzeQuery } from "@/lib/site-finder-api";
|
import { useParcelAnalyzeQuery } from "@/lib/site-finder-api";
|
||||||
import type { UtilitySummaryItem } from "@/lib/site-finder-api";
|
import type { UtilitySummaryItem } from "@/lib/site-finder-api";
|
||||||
|
|
||||||
|
|
@ -487,17 +488,33 @@ export function Section2NetworksUtilities({ cad }: Props) {
|
||||||
marginBottom: 24,
|
marginBottom: 24,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Section title */}
|
{/* Section title + verify-link на НСПД (#97) */}
|
||||||
<h2
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: 12,
|
||||||
|
flexWrap: "wrap",
|
||||||
margin: "0 0 12px",
|
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 */}
|
{/* HeadlineBar */}
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
|
|
||||||
27
frontend/src/lib/nspdLinks.ts
Normal file
27
frontend/src/lib/nspdLinks.ts
Normal 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)}`;
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue