"use client"; import dynamic from "next/dynamic"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useRef, useState } from "react"; import { CadInput } from "@/components/site-finder/CadInput"; 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 { addLocalRecentParcel, 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: () => (
Загрузка карты...
), }, ); // ── 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) { // useParcelsBboxQuery has no keepPreviousData (unlike useParcelAnalyzeQuery), // and its queryKey carries the bbox — so on every pan/zoom both `data` go // undefined mid-fetch. We hold the last fully-resolved pair so the counter // doesn't flash "0 / 0" while fetching (#1419), and so totalCount/matchCount // are always taken from the *same* committed frame — never a new bbox's // matchCount over an old bbox's totalCount, which could read matchCount > // totalCount and break the X ⊆ Y invariant (#1420). const allQuery = useParcelsBboxQuery(bbox, {}); const filteredQuery = useParcelsBboxQuery(bbox, filters); const lastCounts = useRef<{ totalCount: number; matchCount: number } | null>( null, ); // Only commit a new pair once BOTH queries have data for the current render; // until then keep the previous pair (placeholderData: keepPreviousData // equivalent, scoped to this bridge). if (allQuery.data != null && filteredQuery.data != null) { lastCounts.current = { totalCount: allQuery.data.length, matchCount: filteredQuery.data.length, }; } const counts = lastCounts.current ?? { totalCount: 0, matchCount: 0 }; return ( ); } // ── Page ────────────────────────────────────────────────────────────────────── export default function SiteFinderPage() { const router = useRouter(); const [filters, setFilters] = useState({}); const [selectedParcel, setSelectedParcel] = useState( null, ); const [bbox, setBbox] = useState(null); function handleParcelSelect(parcel: ParcelBboxItem) { setSelectedParcel(parcel); } function handleParcelDeselect() { setSelectedParcel(null); } function handleParcelOpen(parcel: ParcelBboxItem) { // Record the visit so RecentParcels stays populated, then navigate. addLocalRecentParcel({ cad_num: parcel.cad_num, address: parcel.address, area_ha: parcel.area_ha, district: parcel.district, visited_at: new Date().toISOString(), }); router.push(`/site-finder/analysis/${encodeURIComponent(parcel.cad_num)}`); } function handleCadSubmit(cad: string) { router.push(`/site-finder/analysis/${encodeURIComponent(cad)}`); } return (
{/* Header */}
← Главная ·

SiteFinder · карта участков

Сравнить участки
{/* Filter bar */}
{/* Main layout. На планшете/телефоне .gd-split встаёт в колонку (карта над сайдбаром), .gd-map-shell держит высоту карты (issue #66). */}
{/* Map area — position: relative so ParcelDrawer can anchor absolutely here */}
{/* ParcelDrawer — anchored to map area right edge, not page */}
{/* Right sidebar — always visible; CadInput accessible parallel with drawer */}
); }