feat(tradein): ui-preview/estimate — все 17 секций на оффлайн-фикстуре (#801)
Расширяет preview-маршрут до 1:1 с реальной страницей результата: - fixture.ts: добавлены FIXTURE_IMV, FIXTURE_HOUSES, FIXTURE_PLACEMENT, FIXTURE_ANALYTICS, FIXTURE_SELLTIME с правдоподобными данными (ЕКБ, 2-к ~9 млн) - page.tsx: все 17 секций в порядке page.tsx строки 228-276; QueryClient засевает placement-history / house-analytics / sell-time-sensitivity / cian-price-changes — ни одна секция не схлопывается в return null Секции: SourcesProgress, WhatIfPanel, HeroSummary, IMVBenchmark, CianValuationCard, HouseInfoCard, PlacementHistoryCard, HouseAnalyticsSection, PhotoUpload, ListingsCard, DealsCard, StreetDealsCard, MapCard, DistributionCard, PriceTrendCard, ExposureCard, OfferCard Closes #801
This commit is contained in:
parent
1a733302c2
commit
a34686ac38
2 changed files with 263 additions and 4 deletions
|
|
@ -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 },
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue