feat(tradein): ui-preview/estimate — все 17 секций на оффлайн-фикстуре (#801) #828

Merged
bot-reviewer merged 1 commit from feat/801-preview-full-wt into main 2026-05-30 19:57:05 +00:00
2 changed files with 263 additions and 4 deletions

View file

@ -5,7 +5,16 @@
// photo_url=null у всех лотов — намеренно, чтобы не тянуть внешние картинки
// (чистый оффлайн-рендер для lighthouse/axe).
import type { AggregatedEstimate, AnalogLot, TradeInEstimateInput } from "@/types/trade-in";
import type {
AggregatedEstimate,
AnalogLot,
HouseAnalyticsResponse,
HouseInfoForEstimate,
IMVBenchmarkResponse,
PlacementHistoryItem,
SellTimeSensitivityResponse,
TradeInEstimateInput,
} from "@/types/trade-in";
const analogs: AnalogLot[] = [
{
@ -187,3 +196,163 @@ export const FIXTURE_INPUT: TradeInEstimateInput = {
repair_state: "good",
has_balcony: true,
};
// ── IMV Benchmark fixture ─────────────────────────────────────────────────────
// available:true обязательно — иначе IMVBenchmark возвращает null.
export const FIXTURE_IMV: IMVBenchmarkResponse = {
available: true,
cache_key: "ekb:preview-house:2k",
recommended_price: 10_200_000,
lower_price: 9_400_000,
higher_price: 11_000_000,
market_count: 7,
fetched_at: "2026-05-30T12:00:00Z",
our_median_price: 9_850_000,
diff_pct: -3.4,
};
// ── HouseInfo fixture ─────────────────────────────────────────────────────────
// houses.length > 0 обязательно — иначе HouseInfoCard возвращает null.
export const FIXTURE_HOUSES: HouseInfoForEstimate[] = [
{
house_id: 42001,
ext_house_id: "ekb-repin-75",
address: "Екатеринбург, ул. Репина, 75",
short_address: "Репина, 75",
lat: 56.84,
lon: 60.57,
year_built: 2018,
total_floors: 11,
house_type: "monolith",
passenger_elevators: 2,
cargo_elevators: 1,
has_concierge: false,
closed_yard: true,
has_playground: true,
parking_type: "подземная",
developer_name: "Атомстройкомплекс",
rating: 4.3,
reviews_count: 28,
raw_characteristics: [],
},
];
// ── PlacementHistory fixture ──────────────────────────────────────────────────
// items.length > 0 обязательно — иначе PlacementHistoryCard возвращает null.
export const FIXTURE_PLACEMENT: PlacementHistoryItem[] = [
{
id: 1001,
source: "avito",
house_id: 42001,
ext_item_id: "avito-preview-1001",
title: "2к, 54 м², 7/16 эт.",
rooms: 2,
area_m2: 54,
floor: 7,
total_floors: 16,
start_price: 10_200_000,
start_price_date: "2025-11-01",
last_price: 9_700_000,
last_price_date: "2026-03-15",
removed_date: "2026-04-10",
exposure_days: 160,
notes: null,
},
{
id: 1002,
source: "cian",
house_id: 42001,
ext_item_id: "cian-preview-1002",
title: "2к, 57 м², 4/10 эт.",
rooms: 2,
area_m2: 57,
floor: 4,
total_floors: 10,
start_price: 9_800_000,
start_price_date: "2025-09-10",
last_price: 9_500_000,
last_price_date: "2025-12-20",
removed_date: "2026-01-05",
exposure_days: 117,
notes: null,
},
{
id: 1003,
source: "avito",
house_id: 42001,
ext_item_id: "avito-preview-1003",
title: "2к, 55 м², 5/11 эт.",
rooms: 2,
area_m2: 55,
floor: 5,
total_floors: 11,
start_price: 9_500_000,
start_price_date: "2025-06-01",
last_price: 9_100_000,
last_price_date: "2025-08-30",
removed_date: "2025-09-05",
exposure_days: 96,
notes: null,
},
];
// ── HouseAnalytics fixture ────────────────────────────────────────────────────
// kpi.total_lots > 0 обязательно — иначе HouseAnalyticsSection возвращает null.
// price_history >= 2 точки — для рендера PriceHistoryChart.
// recent_sold > 0 — для рендера RecentSoldList.
export const FIXTURE_ANALYTICS: HouseAnalyticsResponse = {
house_ids: [42001],
radius_m: 0,
kpi: {
total_lots: 38,
sold_count: 24,
sold_rate_pct: 63.2,
median_exposure_days: 112,
median_bargain_pct: 4.8,
},
price_history: [
{ year: 2022, source: "avito_imv", median_price_per_m2: 155_000, n_lots: 6, median_price_rub: 8_525_000 },
{ year: 2023, source: "avito_imv", median_price_per_m2: 161_000, n_lots: 9, median_price_rub: 8_855_000 },
{ year: 2024, source: "avito_imv", median_price_per_m2: 170_000, n_lots: 12, median_price_rub: 9_350_000 },
{ year: 2025, source: "avito_imv", median_price_per_m2: 176_000, n_lots: 11, median_price_rub: 9_680_000 },
],
recent_sold: [
{
id: 2001,
source: "avito_imv",
rooms: 2,
area_m2: 54,
floor: 7,
start_price: 10_200_000,
last_price: 9_700_000,
removed_date: "2026-04-10",
exposure_days: 160,
discount_pct: 4.9,
},
{
id: 2002,
source: "avito_imv",
rooms: 2,
area_m2: 57,
floor: 4,
start_price: 9_800_000,
last_price: 9_500_000,
removed_date: "2026-01-05",
exposure_days: 117,
discount_pct: 3.1,
},
],
};
// ── SellTimeSensitivity fixture ───────────────────────────────────────────────
export const FIXTURE_SELLTIME: SellTimeSensitivityResponse = {
house_ids: [42001],
radius_m: 0,
target_median_price_per_m2: 178_000,
buckets: [
{ price_premium_label: "cheap", price_premium_pct: -5, median_exposure_days: 38, p25_days: 21, p75_days: 55, n_lots: 8 },
{ price_premium_label: "median", price_premium_pct: 0, median_exposure_days: 112, p25_days: 60, p75_days: 160, n_lots: 14 },
{ price_premium_label: "plus5", price_premium_pct: 5, median_exposure_days: 185, p25_days: 90, p75_days: 240, n_lots: 10 },
{ price_premium_label: "plus10", price_premium_pct: 10, median_exposure_days: 270, p25_days: 150, p75_days: 360, n_lots: 6 },
],
};

View file

@ -15,6 +15,25 @@
* Локально: echo NEXT_PUBLIC_ENABLE_PREVIEW=1 > .env.local && npm run dev
* http://localhost:3000/ui-preview/estimate
* CI: собрать с флагом, поднять next start, навести axe/lighthouse.
*
* Секции (1:1 с page.tsx строки 228-276):
* 1. SourcesProgress estimate prop
* 2. WhatIfPanel baseEstimate + baseInput props
* 3. HeroSummary estimate + input props
* 4. IMVBenchmark benchmark prop (FIXTURE_IMV, available:true)
* 5. CianValuationCard data prop
* 6. HouseInfoCard houses prop (FIXTURE_HOUSES, length>0)
* 7. PlacementHistoryCard estimateId; seed queryKey "placement-history"
* 8. HouseAnalyticsSection estimateId; seed "house-analytics" + "sell-time-sensitivity"
* 9. PhotoUpload estimateId (upload-UI; fetch photo может тихо упасть ок)
* 10. ListingsCard estimate + estimateId; seed "cian-price-changes" пустым
* 11. DealsCard estimate prop
* 12. StreetDealsCard estimate prop
* 13. MapCard estimate prop (analogs имеют lat/lon пины есть)
* 14. DistributionCard estimate prop
* 15. PriceTrendCard estimate prop
* 16. ExposureCard estimate prop
* 17. OfferCard estimate + brandSlug=null props
*/
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
@ -27,14 +46,34 @@ import { DealsCard } from "@/components/trade-in/DealsCard";
import { DistributionCard } from "@/components/trade-in/DistributionCard";
import { ExposureCard } from "@/components/trade-in/ExposureCard";
import { HeroSummary } from "@/components/trade-in/HeroSummary";
import { HouseAnalyticsSection } from "@/components/trade-in/HouseAnalyticsSection";
import { HouseInfoCard } from "@/components/trade-in/HouseInfoCard";
import { IMVBenchmark } from "@/components/trade-in/IMVBenchmark";
import { ListingsCard } from "@/components/trade-in/ListingsCard";
import { MapCard } from "@/components/trade-in/MapCard";
import { OfferCard } from "@/components/trade-in/OfferCard";
import { PhotoUpload } from "@/components/trade-in/PhotoUpload";
import { PlacementHistoryCard } from "@/components/trade-in/PlacementHistoryCard";
import { PriceTrendCard } from "@/components/trade-in/PriceTrendCard";
import { SourcesProgress } from "@/components/trade-in/SourcesProgress";
import { StreetDealsCard } from "@/components/trade-in/StreetDealsCard";
import { WhatIfPanel } from "@/components/trade-in/WhatIfPanel";
import { ME_QUERY_KEY, type UserScope } from "@/lib/useMe";
import { FIXTURE_ESTIMATE, FIXTURE_INPUT } from "./fixture";
import {
FIXTURE_ANALYTICS,
FIXTURE_ESTIMATE,
FIXTURE_HOUSES,
FIXTURE_IMV,
FIXTURE_INPUT,
FIXTURE_PLACEMENT,
FIXTURE_SELLTIME,
} from "./fixture";
const ENABLED = process.env.NEXT_PUBLIC_ENABLE_PREVIEW === "1";
const PREVIEW_ID = FIXTURE_ESTIMATE.estimate_id;
// Фейковый authorized-юзер: засеивается в QueryClient → useMe/useBrand резолвятся
// оффлайн (никакого /me, /quota, /brand), generic-бренд (brand=null).
const PREVIEW_USER: UserScope = {
@ -52,7 +91,15 @@ function PreviewContent() {
queries: { retry: false, staleTime: Infinity, refetchOnWindowFocus: false },
},
});
// /me — пропускает RouteGuard без сетевого запроса
qc.setQueryData(ME_QUERY_KEY, PREVIEW_USER);
// Секция 7: PlacementHistoryCard — useEstimatePlacementHistory
qc.setQueryData(["trade-in", "estimate", PREVIEW_ID, "placement-history"], FIXTURE_PLACEMENT);
// Секция 8: HouseAnalyticsSection — useEstimateHouseAnalytics + useEstimateSellTimeSensitivity
qc.setQueryData(["trade-in", "estimate", PREVIEW_ID, "house-analytics"], FIXTURE_ANALYTICS);
qc.setQueryData(["trade-in", "estimate", PREVIEW_ID, "sell-time-sensitivity"], FIXTURE_SELLTIME);
// Секция 10: ListingsCard — useEstimateCianPriceChanges (пустой = graceful)
qc.setQueryData(["trade-in", "estimate", PREVIEW_ID, "cian-price-changes"], []);
return qc;
});
@ -62,18 +109,61 @@ function PreviewContent() {
(flex-column gap, --container ширина). Без них карточки рендерились узко/«мобильно». */}
<main className="page">
<section className="result-col">
{/* 1. SourcesProgress */}
<SourcesProgress estimate={FIXTURE_ESTIMATE} isPending={false} />
{/* 2. WhatIfPanel */}
<WhatIfPanel baseEstimate={FIXTURE_ESTIMATE} baseInput={FIXTURE_INPUT} />
{/* 3. HeroSummary */}
<HeroSummary
estimate={FIXTURE_ESTIMATE}
input={FIXTURE_INPUT}
onResubmit={() => {}}
isResubmitting={false}
/>
{/* 4. IMVBenchmark — передаём пропом напрямую (available:true → не null) */}
<IMVBenchmark benchmark={FIXTURE_IMV} isLoading={false} />
{/* 5. CianValuationCard */}
<CianValuationCard data={FIXTURE_ESTIMATE.cian_valuation} />
<DistributionCard estimate={FIXTURE_ESTIMATE} />
{/* 6. HouseInfoCard — передаём пропом напрямую (length>0 → не null) */}
<HouseInfoCard houses={FIXTURE_HOUSES} isLoading={false} />
{/* 7. PlacementHistoryCard — useEstimatePlacementHistory; queryKey засеян выше */}
<PlacementHistoryCard estimateId={PREVIEW_ID} />
{/* 8. HouseAnalyticsSection — useEstimateHouseAnalytics; queryKeys засеяны выше */}
<HouseAnalyticsSection estimateId={PREVIEW_ID} />
{/* 9. PhotoUpload — рендерит upload-UI; fetch фото может тихо упасть — ок */}
<PhotoUpload estimateId={PREVIEW_ID} />
{/* 10. ListingsCard — cian-price-changes засеян пустым массивом */}
<ListingsCard estimate={FIXTURE_ESTIMATE} estimateId={PREVIEW_ID} />
{/* 11. DealsCard */}
<DealsCard estimate={FIXTURE_ESTIMATE} />
{/* 12. StreetDealsCard */}
<StreetDealsCard estimate={FIXTURE_ESTIMATE} />
<ExposureCard estimate={FIXTURE_ESTIMATE} />
{/* 13. MapCard — analogs имеют lat/lon → пины есть; Leaflet CDN → тайлы из сети (ок) */}
<MapCard estimate={FIXTURE_ESTIMATE} />
{/* 14. DistributionCard */}
<DistributionCard estimate={FIXTURE_ESTIMATE} />
{/* 15. PriceTrendCard */}
<PriceTrendCard estimate={FIXTURE_ESTIMATE} />
{/* 16. ExposureCard */}
<ExposureCard estimate={FIXTURE_ESTIMATE} />
{/* 17. OfferCard */}
<OfferCard estimate={FIXTURE_ESTIMATE} brandSlug={null} />
</section>
</main>
</QueryClientProvider>