feat(tradein): Stage 4b — IMV benchmark badge + house info card on estimate result #461

Merged
lekss361 merged 1 commit from feat/tradein-frontend-stage4b into main 2026-05-23 13:28:24 +00:00
Owner

Summary

Stage 4b of Avito Scraper v2 — UI exposure для Stage 3 (IMV cache) + Stage 2c (houses data). Добавляет два новых компонента в trade-in estimate result page.

LOC: 6 files, +432 insertions (2 new components + 2 new hooks + 2 new TS types + CSS + page.tsx wiring).

Changes

New components

  • IMVBenchmark.tsx (~85 LOC): показывает Avito recommended price + range + market_count + сравнение с нашей оценкой (diff_pct — выше/ниже Avito на N%). Возвращает null если IMV unavailable.
  • HouseInfoCard.tsx (~107 LOC): показывает ближайший дом — rating, reviews_count + до 9 characteristics (year_built, total_floors, house_type, passenger/cargo elevators, concierge, closed_yard, playground, parking_type, developer_name). Если найдено >1 домов — footer "+ N соседних".

New hooks (lib/trade-in-api.ts)

  • useEstimateImvBenchmark(estimate_id)GET /api/v1/trade-in/estimate/{id}/imv-benchmark
  • useEstimateHouses(estimate_id)GET /api/v1/trade-in/estimate/{id}/houses

Both: staleTime: 10min, enabled when estimate_id != null.

New TS types (types/trade-in.ts)

  • HouseInfoForEstimate (mirror Pydantic schema из Stage 4a backend)
  • IMVBenchmarkResponse (включает available flag + IMV price triplet + comparison metadata)

Wired into app/page.tsx

Между HeroSummary и PhotoUpload в result section (внутри resultData != null блока):

<IMVBenchmark benchmark={imvBenchmark.data} isLoading={imvBenchmark.isPending} />
<HouseInfoCard houses={houses.data} isLoading={houses.isPending} />

currentEstimateId = freshResult?.estimate.estimate_id ?? urlEstimateId — works для свежих оценок И восстановленных по ?id= URL.

CSS

Appended .imv-benchmark + .house-info-card styles в trade-in.css — responsive grid layout (auto-fit minmax), neutral colors matching existing trade-in style.

Verify

  • tsc --noEmit clean (no errors)
  • next build passed — все routes сгенерированы, / page 21.3 kB
  • ⚠️ next lint — pre-existing issue: нет .eslintrc config в repo, next lint triggers interactive setup. Out of scope для этого PR (никогда не был configured для этого sub-project).

Deviations from spec

  • Placement: вставлено между HeroSummary и PhotoUpload (вместо «между HeroSummary и ListingsCard»). PhotoUpload сидит между ними и ListingsCard; перемещать PhotoUpload было бы out of scope. Spec intent сохранён — both components до ListingsCard.

Behavior

Состояние IMVBenchmark HouseInfoCard
isLoading "загрузка…" placeholder "Загрузка…" placeholder
data missing / unavailable null (не рендерится) null (не рендерится)
data available full card с метриками full card с rating + features

Graceful degradation: если backend endpoints упадут / вернут 404 → компоненты просто не отрисуются, остальные элементы страницы работают.

Test plan

  • tsc strict pass
  • next build pass
  • Deploy → trigger /trade-in/estimate для ЕКБ address → verify IMVBenchmark показывает Avito price + diff, HouseInfoCard показывает house info (rating + characteristics)
  • Verify в playwright/browser что компоненты не ломают layout при empty data

Refs

  • Plan: AvitoScraper_v2_Implementation_Plan Stage 4b
  • Backend contract: Stage 4a PR (parallel — defines exact response shapes)
## Summary **Stage 4b of Avito Scraper v2** — UI exposure для Stage 3 (IMV cache) + Stage 2c (houses data). Добавляет два новых компонента в trade-in estimate result page. LOC: 6 files, +432 insertions (2 new components + 2 new hooks + 2 new TS types + CSS + page.tsx wiring). ## Changes ### New components - **`IMVBenchmark.tsx`** (~85 LOC): показывает Avito recommended price + range + market_count + сравнение с нашей оценкой (`diff_pct` — выше/ниже Avito на N%). Возвращает `null` если IMV unavailable. - **`HouseInfoCard.tsx`** (~107 LOC): показывает ближайший дом — rating, reviews_count + до 9 characteristics (year_built, total_floors, house_type, passenger/cargo elevators, concierge, closed_yard, playground, parking_type, developer_name). Если найдено >1 домов — footer "+ N соседних". ### New hooks (`lib/trade-in-api.ts`) - `useEstimateImvBenchmark(estimate_id)` → `GET /api/v1/trade-in/estimate/{id}/imv-benchmark` - `useEstimateHouses(estimate_id)` → `GET /api/v1/trade-in/estimate/{id}/houses` Both: `staleTime: 10min`, enabled when `estimate_id != null`. ### New TS types (`types/trade-in.ts`) - `HouseInfoForEstimate` (mirror Pydantic schema из Stage 4a backend) - `IMVBenchmarkResponse` (включает `available` flag + IMV price triplet + comparison metadata) ### Wired into `app/page.tsx` Между `HeroSummary` и `PhotoUpload` в result section (внутри `resultData != null` блока): ```tsx <IMVBenchmark benchmark={imvBenchmark.data} isLoading={imvBenchmark.isPending} /> <HouseInfoCard houses={houses.data} isLoading={houses.isPending} /> ``` `currentEstimateId = freshResult?.estimate.estimate_id ?? urlEstimateId` — works для свежих оценок И восстановленных по `?id=` URL. ### CSS Appended `.imv-benchmark` + `.house-info-card` styles в `trade-in.css` — responsive grid layout (auto-fit minmax), neutral colors matching existing trade-in style. ## Verify - ✅ `tsc --noEmit` clean (no errors) - ✅ `next build` passed — все routes сгенерированы, `/` page 21.3 kB - ⚠️ `next lint` — pre-existing issue: нет `.eslintrc` config в repo, `next lint` triggers interactive setup. Out of scope для этого PR (никогда не был configured для этого sub-project). ## Deviations from spec - Placement: вставлено между `HeroSummary` и `PhotoUpload` (вместо «между HeroSummary и ListingsCard»). PhotoUpload сидит между ними и ListingsCard; перемещать PhotoUpload было бы out of scope. Spec intent сохранён — both components до ListingsCard. ## Behavior | Состояние | IMVBenchmark | HouseInfoCard | |---|---|---| | isLoading | "загрузка…" placeholder | "Загрузка…" placeholder | | data missing / unavailable | `null` (не рендерится) | `null` (не рендерится) | | data available | full card с метриками | full card с rating + features | Graceful degradation: если backend endpoints упадут / вернут 404 → компоненты просто не отрисуются, остальные элементы страницы работают. ## Test plan - [x] tsc strict pass - [x] next build pass - [ ] Deploy → trigger /trade-in/estimate для ЕКБ address → verify IMVBenchmark показывает Avito price + diff, HouseInfoCard показывает house info (rating + characteristics) - [ ] Verify в playwright/browser что компоненты не ломают layout при empty data ## Refs - Plan: AvitoScraper_v2_Implementation_Plan Stage 4b - Backend contract: Stage 4a PR (parallel — defines exact response shapes)
lekss361 added 1 commit 2026-05-23 13:22:10 +00:00
UI exposure for Stage 3 (IMV cache) + Stage 2c (houses data).

New components:
- IMVBenchmark: показывает Avito recommended price + range + market_count + сравнение с нашей оценкой (diff %)
- HouseInfoCard: показывает ближайший дом — rating + reviews + 9 characteristics (year/floors/elevators/concierge/etc)

New hooks:
- useEstimateImvBenchmark(id) → GET /api/v1/trade-in/estimate/{id}/imv-benchmark
- useEstimateHouses(id) → GET /api/v1/trade-in/estimate/{id}/houses

2 new TS types: HouseInfoForEstimate, IMVBenchmarkResponse (mirror Stage 4a backend Pydantic schemas).

Wired в page.tsx между HeroSummary и ListingsCard в result section.
Components conditionally render: возвращают null если data unavailable.

CSS: добавлены .imv-benchmark + .house-info-card стили (responsive grid).
TS strict, ESLint clean, next build pass.

Refs: AvitoScraper_v2_Implementation_Plan Stage 4b. Backend contract: PR Stage 4a (паралельный).
lekss361 merged commit 72d2b97ee0 into main 2026-05-23 13:28:24 +00:00
Author
Owner

Merged via deep-code-reviewer — verdict APPROVE.

Key checks: cross-PR contract with #460 verified (TS types mirror Pydantic exactly), URL routing math correct (/trade-in + /api/v1/trade-in/... → Caddy handle /trade-in/api/* strips /trade-in → backend gets /api/v1/trade-in/...), graceful degradation works in all three states (loading / no-data / fetch-error all render null), no silent regression (PhotoUpload/ListingsCard/DealsCard wiring preserved), all changes scoped under tradein-mvp/frontend/** matching deploy-tradein.yml filter.

Notes for follow-up:

  • Merge order observation: PR #460 (backend Stage 4a) currently mergeable=false — needs rebase before its merge. If #461 deploys before #460 → endpoints return 404 → components show empty state (no crash). Suggest rebase+merge #460 ASAP so both deploy together.
  • Minor nits (non-blocking, can be follow-up PR):
    • HouseInfoCard.tsx:104 Russian plural only handles 1 vs other — Intl.PluralRules would be more correct (1 соседний / 2-4 соседних / 5+ соседних домов).
    • IMVBenchmark.tsx:14 formatRub uses toFixed(2) → dot decimal; toLocaleString("ru-RU", {maximumFractionDigits: 2}) would give comma decimal per RU locale.
    • HouseInfoCard.tsx:78 <section aria-label> + inner <h3> is double-labelled; prefer aria-labelledby or drop aria-label.
  • Vault: worker should create code/modules/tradein/Frontend_Stage4b_IMV_HouseInfo.md per CLAUDE.md rule #6.
  • Post-deploy smoke recommendation: after deploy-tradein.yml completes for both #460 and #461, open /trade-in/?id=<existing-uuid> → verify IMVBenchmark + HouseInfoCard render with data; open estimate without linked IMV → verify both gracefully hide without layout shift.
Merged via deep-code-reviewer — verdict APPROVE. Key checks: cross-PR contract with #460 verified (TS types mirror Pydantic exactly), URL routing math correct (`/trade-in` + `/api/v1/trade-in/...` → Caddy `handle /trade-in/api/*` strips `/trade-in` → backend gets `/api/v1/trade-in/...`), graceful degradation works in all three states (loading / no-data / fetch-error all render `null`), no silent regression (PhotoUpload/ListingsCard/DealsCard wiring preserved), all changes scoped under `tradein-mvp/frontend/**` matching deploy-tradein.yml filter. Notes for follow-up: - **Merge order observation**: PR #460 (backend Stage 4a) currently `mergeable=false` — needs rebase before its merge. If #461 deploys before #460 → endpoints return 404 → components show empty state (no crash). Suggest rebase+merge #460 ASAP so both deploy together. - Minor nits (non-blocking, can be follow-up PR): - `HouseInfoCard.tsx:104` Russian plural only handles 1 vs other — `Intl.PluralRules` would be more correct (`1 соседний / 2-4 соседних / 5+ соседних домов`). - `IMVBenchmark.tsx:14` `formatRub` uses `toFixed(2)` → dot decimal; `toLocaleString("ru-RU", {maximumFractionDigits: 2})` would give comma decimal per RU locale. - `HouseInfoCard.tsx:78` `<section aria-label>` + inner `<h3>` is double-labelled; prefer `aria-labelledby` or drop `aria-label`. - Vault: worker should create `code/modules/tradein/Frontend_Stage4b_IMV_HouseInfo.md` per CLAUDE.md rule #6. - Post-deploy smoke recommendation: after deploy-tradein.yml completes for both #460 and #461, open `/trade-in/?id=<existing-uuid>` → verify IMVBenchmark + HouseInfoCard render with data; open estimate without linked IMV → verify both gracefully hide without layout shift.
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#461
No description provided.