UI-аудит инструментами (axe/lighthouse) требует отрендеренную контентную
страницу. Live trade-in за auth-gate, локальный фронт без бэкенда = белый экран
(RouteGuard висит на pending /me). Добавлен оффлайн preview «денежного экрана».
- src/app/ui-preview/estimate/{page,fixture}.tsx — рендерит реальные компоненты
результата (HeroSummary + Cian + Distribution + Deals + StreetDeals + Exposure
+ PriceTrend) на статичной фикстуре AggregatedEstimate, без сети: свой
QueryClient с засеянным фейковым /me (useMe/useBrand резолвятся оффлайн).
- RouteGuard: bypass /ui-preview/* под флагом (RBAC к preview не применяется).
- Гейт: NEXT_PUBLIC_ENABLE_PREVIEW=1 (иначе notFound) — в проде по умолчанию off.
Папка ui-preview (НЕ _-prefix: Next трактует _* как private/non-routable).
Проверено локально (NEXT_PUBLIC_ENABLE_PREVIEW=1): страница рендерится оффлайн,
axe (wcag2a/aa) дал baseline — 2 serious: color-contrast (teal #14b8a6 на светлом
2.32<4.5) + svg-img-alt (recharts scatter без accessible text). tsc clean.
Refs #801. Followup: фиксы найденных axe-дефектов отдельными issue.
84 lines
3.5 KiB
TypeScript
84 lines
3.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.
|
||
*/
|
||
|
||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||
import { notFound } from "next/navigation";
|
||
import { useState } from "react";
|
||
|
||
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 { PriceTrendCard } from "@/components/trade-in/PriceTrendCard";
|
||
import { StreetDealsCard } from "@/components/trade-in/StreetDealsCard";
|
||
import { ME_QUERY_KEY, type UserScope } from "@/lib/useMe";
|
||
|
||
import { FIXTURE_ESTIMATE, FIXTURE_INPUT } from "./fixture";
|
||
|
||
const ENABLED = process.env.NEXT_PUBLIC_ENABLE_PREVIEW === "1";
|
||
|
||
// Фейковый 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 },
|
||
},
|
||
});
|
||
qc.setQueryData(ME_QUERY_KEY, PREVIEW_USER);
|
||
return qc;
|
||
});
|
||
|
||
return (
|
||
<QueryClientProvider client={client}>
|
||
<main
|
||
className="tradein-results"
|
||
style={{ maxWidth: 920, margin: "0 auto", padding: 24 }}
|
||
>
|
||
<HeroSummary
|
||
estimate={FIXTURE_ESTIMATE}
|
||
input={FIXTURE_INPUT}
|
||
onResubmit={() => {}}
|
||
isResubmitting={false}
|
||
/>
|
||
<CianValuationCard data={FIXTURE_ESTIMATE.cian_valuation} />
|
||
<DistributionCard estimate={FIXTURE_ESTIMATE} />
|
||
<DealsCard estimate={FIXTURE_ESTIMATE} />
|
||
<StreetDealsCard estimate={FIXTURE_ESTIMATE} />
|
||
<ExposureCard estimate={FIXTURE_ESTIMATE} />
|
||
<PriceTrendCard estimate={FIXTURE_ESTIMATE} />
|
||
</main>
|
||
</QueryClientProvider>
|
||
);
|
||
}
|
||
|
||
export default function PreviewEstimatePage() {
|
||
if (!ENABLED) notFound();
|
||
return <PreviewContent />;
|
||
}
|