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 {
|
||||
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 0–100 с разбором факторов",
|
||||
},
|
||||
{
|
||||
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)",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 { 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 }}>
|
||||
|
|
|
|||
|
|
@ -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 }}>
|
||||
|
|
|
|||
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