All checks were successful
Deploy Trade-In / build-frontend (push) Successful in 1m53s
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / deploy (push) Successful in 36s
Co-authored-by: bot-backend <bot-backend@gendsgn.local> Co-committed-by: bot-backend <bot-backend@gendsgn.local>
208 lines
9.5 KiB
TypeScript
208 lines
9.5 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* #801: dev/CI-only preview-маршрут «денежного экрана» на статичной фикстуре.
|
||
*
|
||
* Зачем: a11y (axe) и lighthouse требуют отрендеренную контентную страницу.
|
||
* Live trade-in за auth-gate, а локальный фронт без бэкенда = белый экран
|
||
* (RouteGuard висит на pending /me). Эта страница рендерит реальные компоненты
|
||
* результата (HeroSummary + карточки/чарты) полностью оффлайн — без сети.
|
||
*
|
||
* Включается ТОЛЬКО при NEXT_PUBLIC_ENABLE_PREVIEW=1 (иначе notFound) — в проде
|
||
* по умолчанию недоступна. RouteGuard пропускает /ui-preview/* под тем же флагом.
|
||
* (Папка НЕ может начинаться с "_" — Next трактует такие как private/non-routable.)
|
||
*
|
||
* Локально: 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";
|
||
import { notFound } from "next/navigation";
|
||
import { useState } from "react";
|
||
|
||
import "@/components/trade-in/trade-in.css";
|
||
import { CianValuationCard } from "@/components/trade-in/CianValuationCard";
|
||
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 { 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_ANALYTICS,
|
||
FIXTURE_ESTIMATE,
|
||
FIXTURE_HOUSES,
|
||
FIXTURE_IMV,
|
||
FIXTURE_INPUT,
|
||
FIXTURE_PLACEMENT,
|
||
FIXTURE_SALES,
|
||
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 = {
|
||
username: "preview",
|
||
role: "admin",
|
||
allowed_paths: ["/**"],
|
||
deny_paths: [],
|
||
brand: null,
|
||
};
|
||
|
||
function PreviewContent() {
|
||
const [client] = useState(() => {
|
||
const qc = new QueryClient({
|
||
defaultOptions: {
|
||
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"], []);
|
||
// Секция 12: StreetDealsCard — useSalesVsListings(address, area_m2, rooms)
|
||
qc.setQueryData(
|
||
[
|
||
"trade-in",
|
||
"sales-vs-listings",
|
||
FIXTURE_ESTIMATE.target_address,
|
||
FIXTURE_ESTIMATE.area_m2,
|
||
FIXTURE_ESTIMATE.rooms,
|
||
],
|
||
FIXTURE_SALES,
|
||
);
|
||
return qc;
|
||
});
|
||
|
||
return (
|
||
<QueryClientProvider client={client}>
|
||
{/* Те же layout-классы, что у реальной страницы (page.tsx): .page → .result-col
|
||
(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} />
|
||
|
||
{/* 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} />
|
||
|
||
{/* 13. MapCard — ОТКЛЮЧЁН в preview: leaflet тянет ~47 тайлов с
|
||
tile.openstreetmap.org + lib с unpkg → внешний network не даёт
|
||
navigation-idle → axe/lighthouse MCP таймаутят (30s). Placeholder
|
||
сохраняет секцию «Карта» (card-структура аудируется), без сети. */}
|
||
<article className="card map-card">
|
||
<div className="card-head">
|
||
<div>
|
||
<div className="section-kicker">Аналитика · Карта</div>
|
||
<h2>Карта аналогов и сделок</h2>
|
||
</div>
|
||
</div>
|
||
<div
|
||
className="card-body"
|
||
style={{
|
||
padding: "48px 24px",
|
||
textAlign: "center",
|
||
color: "var(--muted)",
|
||
}}
|
||
>
|
||
Карта отключена в preview-режиме (внешние OSM-тайлы ломают
|
||
network-idle аудита). На реальной странице здесь leaflet-карта.
|
||
</div>
|
||
</article>
|
||
|
||
{/* 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>
|
||
);
|
||
}
|
||
|
||
export default function PreviewEstimatePage() {
|
||
if (!ENABLED) notFound();
|
||
return <PreviewContent />;
|
||
}
|