"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 */}