gendesign/frontend/src/app/site-finder/page.tsx
Light1YT 8107c23f18
Some checks failed
CI / changes (push) Successful in 8s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Successful in 1m3s
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 50s
Deploy / changes (push) Has been cancelled
Deploy / build-frontend (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Deploy / build-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
feat(frontend): mobile responsiveness (#66) + data-sources страница (#80)
#66: responsive CSS-layer в globals.css (3-контекста <768/768-1280/>1280, не Tailwind
sm/md/lg) + ResizeObserver в ChartShell (единственный echarts импортёр → все charts
рефлоу) + class-применение (grid-collapse, nav horizontal-scroll, map-stack) на
site-finder/analytics. overflow-x guard.
#80: /data-sources attribution-страница (OSM ODbL/ДОМ.РФ/Объектив/Open-Meteo/Росреестр/
НСПД, server component, tokens) + footer-link.

Closes #66
Closes #80
2026-06-13 23:12:31 +05:00

228 lines
6.5 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 { useRouter } from "next/navigation";
import { 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: () => (
<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 router = useRouter();
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);
}
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 (
<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>
<Link
href="/site-finder/compare"
style={{
marginLeft: "auto",
fontSize: 13,
color: "var(--accent)",
textDecoration: "none",
fontWeight: 500,
}}
>
Сравнить участки
</Link>
</header>
{/* Filter bar */}
<div style={{ flexShrink: 0 }}>
<FilterBarBridge filters={filters} onChange={setFilters} bbox={bbox} />
</div>
{/* Main layout. На планшете/телефоне .gd-split встаёт в колонку
(карта над сайдбаром), .gd-map-shell держит высоту карты (issue #66). */}
<div
className="gd-split"
style={{
flex: 1,
display: "flex",
gap: 0,
overflow: "hidden",
minHeight: 0,
}}
>
{/* Map area — position: relative so ParcelDrawer can anchor absolutely here */}
<div
className="gd-map-shell"
style={{
flex: 1,
padding: 16,
display: "flex",
flexDirection: "column",
minHeight: 0,
position: "relative",
}}
>
<EntryMap
filters={filters}
selectedCad={selectedParcel?.cad_num ?? null}
onParcelOpen={handleParcelOpen}
onParcelSelect={handleParcelSelect}
onParcelDeselect={handleParcelDeselect}
onBboxChange={setBbox}
/>
{/* ParcelDrawer — anchored to map area right edge, not page */}
<ParcelDrawer
parcel={selectedParcel}
onClose={handleParcelDeselect}
/>
</div>
{/* Right sidebar — always visible; CadInput accessible parallel with drawer */}
<div
className="gd-split-aside"
style={{
width: 300,
flexShrink: 0,
padding: "16px 16px 16px 0",
display: "flex",
flexDirection: "column",
gap: 12,
overflowY: "auto",
}}
>
<CadInput onSubmit={handleCadSubmit} loading={false} />
<RecentParcels />
<ParcelLegend />
</div>
</div>
</main>
);
}