gendesign/frontend/src/components/site-finder/entry/ParcelDrawer.tsx
Light1YT b3fd053e3d
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 55s
CI / openapi-codegen-check (pull_request) Successful in 1m58s
feat(ptica): map glyph pins + compass, equal-height grids, full-height rail; dark Site Finder
ПТИЦА cockpit:
- map: POI/competitors/connections now render as glyph divIcon pins (per-category
  glyph + color) with dashed connection lines + a compass — matches the prototype
  (was plain CircleMarker dots). Legend/zoom/scale/base-toggle/tools kept.
- lower+bottom grids: align-items stretch → equal-height rows; cards are flex
  columns with footer «Подробнее» pinned to bottom and sparse empty-states
  centred (fixes «выровняй тут все»).
- rail: dropped sticky+height:100vh so it stretches to full page height like the
  prototype (fixes «обрезан»).

Main Site Finder (entry landing) — restyled to the ПТИЦА dark cockpit theme,
scoped to .sfRoot[data-theme=dark] (no leak to light pages): dark CARTO tiles,
Leaflet attribution flag removed (prefix=false, © credit kept), dark markers /
filter chips / legend / recent list / drawer / cad input.
2026-06-21 03:41:13 +05:00

376 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import Link from "next/link";
import { X, MapPin, Maximize2, ArrowRight } from "lucide-react";
import type { ParcelBboxItem } from "@/lib/site-finder-api";
import { addLocalRecentParcel } from "@/lib/site-finder-api";
import { STATUS_COLORS, STATUS_LABELS } from "./ParcelLegend";
interface ParcelDrawerProps {
parcel: ParcelBboxItem | null;
onClose: () => void;
}
const VRI_LABELS: Record<string, string> = {
multistory: "Многоэтажный",
mixed: "Смешанный",
individual: "ИЖС",
office: "Офисный",
other: "Иное",
};
export function ParcelDrawer({ parcel, onClose }: ParcelDrawerProps) {
if (!parcel) return null;
const statusColor = STATUS_COLORS[parcel.status] ?? "#8ba6b3";
const statusLabel = STATUS_LABELS[parcel.status] ?? parcel.status;
return (
// Drawer panel — absolute within the map container (parent has position:relative).
// This keeps the sidebar (CadInput/RecentParcels/ParcelLegend) always visible
// and only overlaps the map area on parcel select.
<aside
style={{
position: "absolute",
top: 16,
right: 16,
bottom: 16,
width: 360,
zIndex: 20,
background: "rgba(12, 30, 42, 0.94)",
border: "1px solid rgba(150, 192, 214, 0.18)",
borderRadius: 12,
display: "flex",
flexDirection: "column",
boxShadow: "0 18px 70px rgba(0, 0, 0, 0.5)",
backdropFilter: "blur(6px)",
}}
role="dialog"
aria-label="Карточка участка"
>
{/* Header */}
<div
style={{
padding: "16px 20px 12px",
borderBottom: "1px solid rgba(150, 192, 214, 0.1)",
display: "flex",
alignItems: "flex-start",
gap: 8,
}}
>
<MapPin
size={16}
strokeWidth={1.5}
style={{
color: "#8ba6b3",
marginTop: 2,
flexShrink: 0,
}}
/>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontSize: 11,
fontWeight: 500,
textTransform: "uppercase",
letterSpacing: "0.04em",
color: "#5f7886",
fontVariantNumeric: "tabular-nums",
}}
>
{parcel.cad_num}
</div>
<div
style={{
fontSize: 13,
color: "#8ba6b3",
marginTop: 2,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{parcel.address}
</div>
</div>
<button
onClick={onClose}
aria-label="Закрыть"
style={{
background: "none",
border: "none",
cursor: "pointer",
padding: 4,
color: "#8ba6b3",
flexShrink: 0,
}}
>
<X size={18} strokeWidth={1.5} />
</button>
</div>
{/* KPI grid */}
<div
style={{
padding: "16px 20px",
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: 12,
}}
>
{/* Status */}
<div
style={{
background: "rgba(4, 12, 18, 0.6)",
border: "1px solid rgba(150, 192, 214, 0.18)",
borderRadius: 8,
padding: "10px 12px",
}}
>
<div
style={{
fontSize: 11,
fontWeight: 500,
textTransform: "uppercase",
letterSpacing: "0.04em",
color: "#8ba6b3",
marginBottom: 4,
}}
>
Статус
</div>
<div
style={{
display: "flex",
alignItems: "center",
gap: 6,
}}
>
<span
style={{
width: 8,
height: 8,
borderRadius: "50%",
background: statusColor,
flexShrink: 0,
}}
/>
<span
style={{
fontSize: 13,
fontWeight: 500,
color: "#dcebf2",
}}
>
{statusLabel}
</span>
</div>
</div>
{/* Area */}
<div
style={{
background: "rgba(4, 12, 18, 0.6)",
border: "1px solid rgba(150, 192, 214, 0.18)",
borderRadius: 8,
padding: "10px 12px",
}}
>
<div
style={{
fontSize: 11,
fontWeight: 500,
textTransform: "uppercase",
letterSpacing: "0.04em",
color: "#8ba6b3",
marginBottom: 4,
}}
>
Площадь
</div>
<div
style={{
fontSize: 18,
fontWeight: 600,
color: "#dcebf2",
fontVariantNumeric: "tabular-nums",
}}
>
{parcel.area_ha != null ? parcel.area_ha.toFixed(2) : "—"}{" "}
<span
style={{
fontSize: 13,
fontWeight: 400,
color: "#8ba6b3",
}}
>
га
</span>
</div>
</div>
{/* District */}
<div
style={{
background: "rgba(4, 12, 18, 0.6)",
border: "1px solid rgba(150, 192, 214, 0.18)",
borderRadius: 8,
padding: "10px 12px",
}}
>
<div
style={{
fontSize: 11,
fontWeight: 500,
textTransform: "uppercase",
letterSpacing: "0.04em",
color: "#8ba6b3",
marginBottom: 4,
}}
>
Район
</div>
<div
style={{
fontSize: 13,
fontWeight: 500,
color: "#dcebf2",
}}
>
{parcel.district}
</div>
</div>
{/* VRI */}
<div
style={{
background: "rgba(4, 12, 18, 0.6)",
border: "1px solid rgba(150, 192, 214, 0.18)",
borderRadius: 8,
padding: "10px 12px",
}}
>
<div
style={{
fontSize: 11,
fontWeight: 500,
textTransform: "uppercase",
letterSpacing: "0.04em",
color: "#8ba6b3",
marginBottom: 4,
}}
>
ВРИ
</div>
<div
style={{
fontSize: 13,
fontWeight: 500,
color: "#dcebf2",
}}
>
{VRI_LABELS[parcel.vri] ?? parcel.vri}
</div>
</div>
{/* Coords */}
<div
style={{
background: "rgba(4, 12, 18, 0.6)",
border: "1px solid rgba(150, 192, 214, 0.18)",
borderRadius: 8,
padding: "10px 12px",
gridColumn: "1 / -1",
}}
>
<div
style={{
fontSize: 11,
fontWeight: 500,
textTransform: "uppercase",
letterSpacing: "0.04em",
color: "#8ba6b3",
marginBottom: 4,
}}
>
Координаты
</div>
<div
style={{
fontSize: 12,
color: "#8ba6b3",
fontVariantNumeric: "tabular-nums",
}}
>
{parcel.lat.toFixed(5)}, {parcel.lon.toFixed(5)}
</div>
</div>
</div>
{/* Spacer */}
<div style={{ flex: 1 }} />
{/* CTA */}
<div
style={{
padding: "16px 20px",
borderTop: "1px solid rgba(150, 192, 214, 0.1)",
display: "flex",
flexDirection: "column",
gap: 8,
}}
>
<Link
href={`/site-finder/analysis/${encodeURIComponent(parcel.cad_num)}`}
onClick={() => {
// Record the visit so this entry path also lands in RecentParcels (#1477).
addLocalRecentParcel({
cad_num: parcel.cad_num,
address: parcel.address,
area_ha: parcel.area_ha,
district: parcel.district,
visited_at: new Date().toISOString(),
});
}}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 8,
background: "rgba(127, 208, 238, 0.16)",
color: "#8ed3ff",
border: "1px solid rgba(127, 208, 238, 0.55)",
borderRadius: 8,
padding: "10px 20px",
fontSize: 14,
fontWeight: 500,
textDecoration: "none",
}}
>
Открыть анализ
<ArrowRight size={16} strokeWidth={1.5} />
</Link>
<a
href={`https://pkk.rosreestr.ru/#/search/cadastralNumber/${encodeURIComponent(parcel.cad_num)}`}
target="_blank"
rel="noopener noreferrer"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 8,
background: "none",
color: "#8ba6b3",
borderRadius: 8,
padding: "8px 20px",
fontSize: 13,
textDecoration: "none",
}}
>
<Maximize2 size={14} strokeWidth={1.5} />
ПКК Росреестр
</a>
</div>
</aside>
);
}