feat(sf-fe-a2): /site-finder entry — EntryMap + filters + drawer + recent #343

Merged
lekss361 merged 1 commit from feat/sf-fe-a2-entry into main 2026-05-17 22:19:18 +00:00
Owner

Summary

SF Frontend Wave 2 parallel со A4. Fill site-finder/page.tsx shell (A1 merged) — карта-first entry с filters + drawer preview + recent list.

Files

NEW components frontend/src/components/site-finder/entry/:

  • EntryMap.tsx — Leaflet карта ЕКБ с CircleMarker парцелей, bbox-aware refetch (TanStack Query), click-to-select, isFetching indicator
  • MapFilterBar.tsx — chip filters (status / area / vri) + district select + counter, bbox-linked
  • ParcelDrawer.tsx — slide-in 360px: 4 KPI (status / area / district / vri) + coords + «Открыть анализ» → /site-finder/analysis/[cad], fallback ПКК Росреестр
  • RecentParcels.tsx — TanStack Query + localStorage gd_recent_parcels merge (B2 server list + local-only)
  • ParcelLegend.tsx — status color legend (free / in_progress / favorite) + STATUS_COLORS export reused в EntryMap

Hooks frontend/src/lib/site-finder-api.ts:

  • useParcelsBboxQuery(bbox) — fetch B1, mock fallback MOCK_PARCELS_BBOX
  • useRecentParcels() — fetch B2 + merge localStorage, getLocalRecentParcels / addLocalRecentParcel

Fixture frontend/src/lib/mocks/parcels-bbox.json — 20 ЕКБ парцелей (6 районов, все статусы, разные ВРИ)

Page wire frontend/src/app/site-finder/page.tsx — replaced placeholders с real components.

Mock toggle

USE_MOCKS=true или MOCK_PARCELS_BBOX=true → fixture (20 парцелей), no backend needed for demo.

Constraints

  • TS strict, TanStack Query (no useEffect+fetch)
  • Lucide icons, CSS tokens
  • pre-commit pass
  • НЕ trog analysis/[cad]/page.tsx (A4), components/ui/* (A3)

Part of plan vault code/patterns/SiteFinder_Frontend_Migration_Plan_May18.md §A2.

## Summary SF Frontend Wave 2 parallel со A4. Fill `site-finder/page.tsx` shell (A1 merged) — карта-first entry с filters + drawer preview + recent list. ## Files **NEW components** `frontend/src/components/site-finder/entry/`: - `EntryMap.tsx` — Leaflet карта ЕКБ с CircleMarker парцелей, bbox-aware refetch (TanStack Query), click-to-select, isFetching indicator - `MapFilterBar.tsx` — chip filters (status / area / vri) + district select + counter, bbox-linked - `ParcelDrawer.tsx` — slide-in 360px: 4 KPI (status / area / district / vri) + coords + «Открыть анализ» → `/site-finder/analysis/[cad]`, fallback ПКК Росреестр - `RecentParcels.tsx` — TanStack Query + localStorage `gd_recent_parcels` merge (B2 server list + local-only) - `ParcelLegend.tsx` — status color legend (free / in_progress / favorite) + `STATUS_COLORS` export reused в EntryMap **Hooks** `frontend/src/lib/site-finder-api.ts`: - `useParcelsBboxQuery(bbox)` — fetch B1, mock fallback `MOCK_PARCELS_BBOX` - `useRecentParcels()` — fetch B2 + merge localStorage, `getLocalRecentParcels` / `addLocalRecentParcel` **Fixture** `frontend/src/lib/mocks/parcels-bbox.json` — 20 ЕКБ парцелей (6 районов, все статусы, разные ВРИ) **Page wire** `frontend/src/app/site-finder/page.tsx` — replaced placeholders с real components. ## Mock toggle `USE_MOCKS=true` или `MOCK_PARCELS_BBOX=true` → fixture (20 парцелей), no backend needed for demo. ## Constraints - ✅ TS strict, TanStack Query (no useEffect+fetch) - ✅ Lucide icons, CSS tokens - ✅ pre-commit pass - ✅ НЕ trog `analysis/[cad]/page.tsx` (A4), `components/ui/*` (A3) Part of plan vault `code/patterns/SiteFinder_Frontend_Migration_Plan_May18.md` §A2.
lekss361 added 1 commit 2026-05-17 22:14:01 +00:00
- 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 на реальные компоненты
lekss361 merged commit 5aab8a97af into main 2026-05-17 22:19:18 +00:00
Author
Owner

Deep Code Review — verdict: APPROVE

Files: 8 (1 modified, 7 new) · Lines: +1629 / -93 · Mergeable: yes · Prior reviews: none

Critical / High / Medium — none

Leaflet SSR safety — CORRECT

page.tsx uses dynamic(() => import("@/components/site-finder/entry/EntryMap").then(m => m.EntryMap), { ssr: false, loading: ... }). Combined with "use client" directive in EntryMap.tsx, this prevents the classic hydration crash from window access inside Leaflet's module init. Loading fallback shows "Загрузка карты..." while the chunk downloads.

Bbox refetch — ACCEPTABLE (follow-up worth)

BboxListener hooks Leaflet's moveend + zoomend (gesture-end events, not continuous move), so requests fire once per pan/zoom — not in a loop. TanStack Query staleTime: 30_000 + structural query key (bbox + filters) prevents re-fetches when nothing changes. Query is gated by enabled: MOCK_PARCELS_BBOX || bbox != null so no fetch with null bbox.

Minor nit (follow-up, not blocking): FilterBarBridge fires two useParcelsBboxQuery calls (one with filters, one without) to compute matchCount / totalCount. For prod B1 backend this doubles QPS per bbox change. Consider either (a) a single query + client-side count or (b) backend ?count_only=1 variant in a later PR. For mock period this is free.

URL safety for cad_num — CORRECT

Both ParcelDrawer.tsx:313 and RecentParcels.tsx:154 use encodeURIComponent(parcel.cad_num) when constructing /site-finder/analysis/{cad} paths and the pkk.rosreestr.ru external link. External anchor has target="_blank" rel="noopener noreferrer". No dangerouslySetInnerHTML anywhere. No injection vector.

Other findings

  • No new deps: react-leaflet@^5.0.0 and leaflet@^1.9.4 already in frontend/package.json.
  • localStorage: try/catch + Array.isArray guard in getLocalRecentParcels; LRU dedup-then-slice in addLocalRecentParcel — atomic R-M-W is fine (single-tab single-thread). Per-item shape not validated (would leak malformed objects from corrupted storage). Low risk, document only.
  • STATUS_COLORS import path: EntryMap and ParcelDrawer both import from ./ParcelLegend — no circular dep, ParcelLegend exports only constants + a self-contained component.
  • TS strict: no any; unknown cast guarded; type-only imports used.
  • Mock toggle wiring: MOCK_PARCELS_BBOX and MOCK_RECENT_PARCELS both exist in mock-toggle.ts (verified).
  • Fixture: 20 parcels, 6 districts, all 3 statuses, 4 ВРИ — matches ParcelBboxItem schema.

Recommendation

Merging now (squash).

Follow-up tickets worth filing (non-blocking):

  1. useDebouncedValue(bbox, 250ms) if real-traffic bbox refetch chatter becomes visible.
  2. Single-query count strategy in FilterBarBridge once B1 backend lands in prod.
  3. Stricter typing: STATUS_COLORS: Record<ParcelStatus, string> instead of Record<string, string>.
## Deep Code Review — verdict: APPROVE **Files**: 8 (1 modified, 7 new) · **Lines**: +1629 / -93 · **Mergeable**: yes · **Prior reviews**: none ### Critical / High / Medium — none ### Leaflet SSR safety — CORRECT `page.tsx` uses `dynamic(() => import("@/components/site-finder/entry/EntryMap").then(m => m.EntryMap), { ssr: false, loading: ... })`. Combined with `"use client"` directive in `EntryMap.tsx`, this prevents the classic hydration crash from `window` access inside Leaflet's module init. Loading fallback shows "Загрузка карты..." while the chunk downloads. ### Bbox refetch — ACCEPTABLE (follow-up worth) `BboxListener` hooks Leaflet's `moveend` + `zoomend` (gesture-end events, not continuous `move`), so requests fire once per pan/zoom — not in a loop. TanStack Query `staleTime: 30_000` + structural query key (bbox + filters) prevents re-fetches when nothing changes. Query is gated by `enabled: MOCK_PARCELS_BBOX || bbox != null` so no fetch with `null` bbox. Minor nit (follow-up, not blocking): `FilterBarBridge` fires **two** `useParcelsBboxQuery` calls (one with filters, one without) to compute `matchCount / totalCount`. For prod B1 backend this doubles QPS per bbox change. Consider either (a) a single query + client-side count or (b) backend `?count_only=1` variant in a later PR. For mock period this is free. ### URL safety for cad_num — CORRECT Both `ParcelDrawer.tsx:313` and `RecentParcels.tsx:154` use `encodeURIComponent(parcel.cad_num)` when constructing `/site-finder/analysis/{cad}` paths and the `pkk.rosreestr.ru` external link. External anchor has `target="_blank" rel="noopener noreferrer"`. No `dangerouslySetInnerHTML` anywhere. No injection vector. ### Other findings - No new deps: `react-leaflet@^5.0.0` and `leaflet@^1.9.4` already in `frontend/package.json`. - `localStorage`: try/catch + `Array.isArray` guard in `getLocalRecentParcels`; LRU dedup-then-slice in `addLocalRecentParcel` — atomic R-M-W is fine (single-tab single-thread). Per-item shape not validated (would leak malformed objects from corrupted storage). Low risk, document only. - `STATUS_COLORS` import path: `EntryMap` and `ParcelDrawer` both import from `./ParcelLegend` — no circular dep, `ParcelLegend` exports only constants + a self-contained component. - TS strict: no `any`; `unknown` cast guarded; type-only imports used. - Mock toggle wiring: `MOCK_PARCELS_BBOX` and `MOCK_RECENT_PARCELS` both exist in `mock-toggle.ts` (verified). - Fixture: 20 parcels, 6 districts, all 3 statuses, 4 ВРИ — matches `ParcelBboxItem` schema. ### Recommendation Merging now (squash). Follow-up tickets worth filing (non-blocking): 1. `useDebouncedValue(bbox, 250ms)` if real-traffic bbox refetch chatter becomes visible. 2. Single-query count strategy in `FilterBarBridge` once B1 backend lands in prod. 3. Stricter typing: `STATUS_COLORS: Record<ParcelStatus, string>` instead of `Record<string, string>`.
Author
Owner

Merged via deep-code-reviewer — verdict APPROVE.

Scope: 8 files (5 NEW components in components/site-finder/entry/, NEW lib/site-finder-api.ts hooks, NEW fixture, wire app/site-finder/page.tsx). +1629/-93 lines.

Highlights:

  • TS strict clean — no any, no @ts-ignore. Single justified eslint-disable react-hooks/exhaustive-deps in BboxListener initial-emit effect.
  • TanStack Query throughout — no useEffect+fetch antipattern. queryKey stable per filter combo, staleTime 30s / 60s reasonable.
  • Leaflet SSR-safe: dynamic(..., { ssr: false }) in page.tsx. useMapEvents handles listener cleanup automatically.
  • localStorage SSR-guarded (typeof window === "undefined" + try/catch around JSON parse/write).
  • XSS: ПКК Росреестр href is a hardcoded literal + encodeURIComponent(cad_num); target="_blank" rel="noopener noreferrer" correct. No innerHTML / dangerouslySetInnerHTML.
  • Mock fallback correct: MOCK_PARCELS_BBOX short-circuits before apiFetch; useRecentParcels gracefully falls back to localStorage on B2 endpoint failure.
  • Zero touches to app/site-finder/analysis/** (A4) or components/ui/** (A3) — no coupling with parallel PR #344.
  • Drawer: click-outside via fixed backdrop OK. Escape handler / focus trap not implemented — acceptable for non-modal preview pane.

Non-blocking nits (consider in follow-up):

  • EntryMap.tsx:121mapRef is set but never read (dead ref, can be removed).
  • page.tsx:54-65FilterBarBridge fires 2 distinct bbox queries (with/without filters) per bbox change. Fine for client-side mock filtering; a single count-aware B1 endpoint could collapse this later.
  • applyFilters (site-finder-api.ts:108) treats vri === "other" as match-all — undocumented escape hatch.
  • RecentParcels uses inline onMouseEnter/Leave style mutation instead of CSS :hover — works, minor consistency item.

Merge commit: 5aab8a97af3b6ee33c472719a96717ba153b8730.

Merged via deep-code-reviewer — verdict APPROVE. Scope: 8 files (5 NEW components in `components/site-finder/entry/`, NEW `lib/site-finder-api.ts` hooks, NEW fixture, wire `app/site-finder/page.tsx`). +1629/-93 lines. Highlights: - TS strict clean — no `any`, no `@ts-ignore`. Single justified `eslint-disable react-hooks/exhaustive-deps` in `BboxListener` initial-emit effect. - TanStack Query throughout — no `useEffect+fetch` antipattern. `queryKey` stable per filter combo, `staleTime` 30s / 60s reasonable. - Leaflet SSR-safe: `dynamic(..., { ssr: false })` in `page.tsx`. `useMapEvents` handles listener cleanup automatically. - localStorage SSR-guarded (`typeof window === "undefined"` + try/catch around JSON parse/write). - XSS: ПКК Росреестр href is a hardcoded literal + `encodeURIComponent(cad_num)`; `target="_blank" rel="noopener noreferrer"` correct. No `innerHTML` / `dangerouslySetInnerHTML`. - Mock fallback correct: `MOCK_PARCELS_BBOX` short-circuits before `apiFetch`; `useRecentParcels` gracefully falls back to localStorage on B2 endpoint failure. - Zero touches to `app/site-finder/analysis/**` (A4) or `components/ui/**` (A3) — no coupling with parallel PR #344. - Drawer: click-outside via fixed backdrop OK. Escape handler / focus trap not implemented — acceptable for non-modal preview pane. Non-blocking nits (consider in follow-up): - `EntryMap.tsx:121` — `mapRef` is set but never read (dead ref, can be removed). - `page.tsx:54-65` — `FilterBarBridge` fires 2 distinct bbox queries (with/without filters) per bbox change. Fine for client-side mock filtering; a single count-aware B1 endpoint could collapse this later. - `applyFilters` (`site-finder-api.ts:108`) treats `vri === "other"` as match-all — undocumented escape hatch. - `RecentParcels` uses inline `onMouseEnter/Leave` style mutation instead of CSS `:hover` — works, minor consistency item. Merge commit: `5aab8a97af3b6ee33c472719a96717ba153b8730`.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#343
No description provided.