gendesign/frontend/src/app/site-finder/page.tsx
lekss361 0084773abf feat(sf-fe-a2): EntryMap + MapFilterBar + ParcelDrawer + RecentParcels + ParcelLegend
- EntryMap: Leaflet карта ЕКБ с CircleMarker парцелей, bbox-aware refetch (TanStack Query),
  click-to-select, deselect on backdrop, isFetching indicator
- MapFilterBar: chip-фильтры (status/area/vri) + district select + counter, bbox-linked
  counter через FilterBarBridge без ремаунта карты
- ParcelDrawer: slide-in panel 360px — 4 KPI (status/area/district/vri) + coords +
  CTA «Открыть анализ» -> /site-finder/analysis/[cad], fallback ПКК Росреестр
- RecentParcels: TanStack Query hook с localStorage fallback (gd_recent_parcels),
  merges B2 server list + local-only items; empty state + hover effects
- ParcelLegend: status color legend (free/in_progress/favorite) + STATUS_COLORS export
  reused in EntryMap markers
- site-finder-api.ts: useParcelsBboxQuery + useRecentParcels hooks, mock fallback via
  MOCK_PARCELS_BBOX / MOCK_RECENT_PARCELS; getLocalRecentParcels / addLocalRecentParcel
- parcels-bbox.json: 20 ЕКБ-парцелей фикстура (6 районов, все статусы, разные ВРИ)
- page.tsx: заменены MapPlaceholder + SidebarPlaceholder на реальные компоненты
2026-05-18 01:12:32 +03:00

184 lines
4.9 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 dynamic from "next/dynamic";
import Link from "next/link";
import { useState } from "react";
import { MapFilterBar } from "@/components/site-finder/entry/MapFilterBar";
import { ParcelDrawer } from "@/components/site-finder/entry/ParcelDrawer";
import { ParcelLegend } from "@/components/site-finder/entry/ParcelLegend";
import { RecentParcels } from "@/components/site-finder/entry/RecentParcels";
import { useParcelsBboxQuery } from "@/lib/site-finder-api";
import type {
ParcelBboxFilters,
ParcelBboxItem,
BboxCoords,
} from "@/lib/site-finder-api";
// EntryMap uses Leaflet — must load without SSR
const EntryMap = dynamic(
() =>
import("@/components/site-finder/entry/EntryMap").then((m) => m.EntryMap),
{
ssr: false,
loading: () => (
<div
style={{
flex: 1,
minHeight: 480,
background: "var(--bg-card-alt)",
border: "1px dashed var(--border-strong)",
borderRadius: 12,
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "var(--fg-tertiary)",
fontSize: 14,
}}
>
Загрузка карты...
</div>
),
},
);
// ── FilterCountBridge ──────────────────────────────────────────────────────────
// Reads parcel counts for current bbox without re-mounting EntryMap.
interface FilterBarBridgeProps {
filters: ParcelBboxFilters;
onChange: (f: ParcelBboxFilters) => void;
bbox: BboxCoords | null;
}
function FilterBarBridge({ filters, onChange, bbox }: FilterBarBridgeProps) {
const { data: allInBbox } = useParcelsBboxQuery(bbox, {});
const { data: filtered } = useParcelsBboxQuery(bbox, filters);
return (
<MapFilterBar
filters={filters}
onChange={onChange}
totalCount={allInBbox?.length ?? 0}
matchCount={filtered?.length ?? 0}
/>
);
}
// ── Page ──────────────────────────────────────────────────────────────────────
export default function SiteFinderPage() {
const [filters, setFilters] = useState<ParcelBboxFilters>({});
const [selectedParcel, setSelectedParcel] = useState<ParcelBboxItem | null>(
null,
);
const [bbox, setBbox] = useState<BboxCoords | null>(null);
function handleParcelSelect(parcel: ParcelBboxItem) {
setSelectedParcel(parcel);
}
function handleParcelDeselect() {
setSelectedParcel(null);
}
return (
<main
style={{
minHeight: "100vh",
background: "var(--bg-app)",
display: "flex",
flexDirection: "column",
}}
>
{/* Header */}
<header
style={{
background: "var(--bg-card)",
borderBottom: "1px solid var(--border-card)",
padding: "12px 24px",
display: "flex",
alignItems: "center",
gap: 16,
flexShrink: 0,
}}
>
<Link
href="/"
style={{
fontSize: 13,
color: "var(--fg-secondary)",
textDecoration: "none",
}}
>
Главная
</Link>
<span style={{ color: "var(--border-soft)" }}>·</span>
<h1
style={{
margin: 0,
fontSize: 16,
fontWeight: 600,
color: "var(--fg-primary)",
}}
>
SiteFinder · карта участков
</h1>
</header>
{/* Filter bar */}
<div style={{ flexShrink: 0 }}>
<FilterBarBridge filters={filters} onChange={setFilters} bbox={bbox} />
</div>
{/* Main layout */}
<div
style={{
flex: 1,
display: "flex",
gap: 0,
overflow: "hidden",
minHeight: 0,
}}
>
{/* Map area */}
<div
style={{
flex: 1,
padding: 16,
display: "flex",
flexDirection: "column",
minHeight: 0,
}}
>
<EntryMap
filters={filters}
selectedCad={selectedParcel?.cad_num ?? null}
onParcelSelect={handleParcelSelect}
onParcelDeselect={handleParcelDeselect}
onBboxChange={setBbox}
/>
</div>
{/* Right sidebar */}
<div
style={{
width: 300,
flexShrink: 0,
padding: "16px 16px 16px 0",
display: "flex",
flexDirection: "column",
gap: 12,
overflowY: "auto",
}}
>
<RecentParcels />
<ParcelLegend />
</div>
</div>
{/* Parcel drawer (slide-in from right) */}
<ParcelDrawer parcel={selectedParcel} onClose={handleParcelDeselect} />
</main>
);
}