gendesign/tradein-mvp/frontend/src/app/ui-preview/estimate/page.tsx
bot-backend 068bd8b5f8
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
fix(tradein): gas leaflet map в ui-preview для CI-аудита (#801) (#837)
Co-authored-by: bot-backend <bot-backend@gendsgn.local>
Co-committed-by: bot-backend <bot-backend@gendsgn.local>
2026-05-30 20:15:58 +00:00

208 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 />;
}