FE-5, last FE wiring on the mappers pattern. Closes #2042. - New hooks (lib/trade-in-api.ts): useEstimateHistory (GET /history), useGeocodeSuggest (GET /geocode/suggest?q=&limit=, debounced, enabled>=3 chars); useQuota reused. - types/trade-in.ts: EstimateHistoryItem (area_m2 string, median_price), GeocodeSuggestion. - mappers.ts: mapCache(history) -> {rows: CacheRow[], kpis: CacheKpi[]} — per-row status by 24h TTL; KPIs FE-derived from /history (всего/средняя цена/повторные %), NOT the global /cache-stats. - CacheView: real previous-estimates list. ParamsPanel: address autocomplete dropdown → captures lat/lon into submit (cleared on manual edit). HeroBar: «Скачать PDF» → /estimate/ {id}/pdf, UUID-gated + safeUrl scheme check + disabled when no estimate. TopNav: «Мои отчёты» from quota.used/history. page.tsx: invalidate history+quota on new estimate. - Hydration-safe: PDF link gated behind a mounted flag (urlId is SSR-null/client-uuid). - FE-1..4 untouched. Verified: next build green (/v2 39.6 kB); tsx harness ran mapCache vs real /history (10 items: ВСЕГО 10 / СРЕДНЯЯ 8,85 млн / ПОВТОРНЫЕ 50%, statuses, null graceful). code-reviewer APPROVE after hydration + reports-fallback fixes.
165 lines
4.5 KiB
TypeScript
165 lines
4.5 KiB
TypeScript
"use client";
|
||
|
||
// Overlay wrapper for the nav-driven section panels of the /trade-in/v2
|
||
// "МЕРА Оценка" design. Faithful port of the design overlay container
|
||
// (МЕРА Оценка.dc.html lines 443-453 + 615-616): a glass/blur absolute layer
|
||
// with HUD corner brackets, a header (section number + title + "← К ОЦЕНКЕ"
|
||
// button) and a scrollable body that swaps the active view.
|
||
|
||
import { tokens } from "./tokens";
|
||
import { overlayNums, overlayTitles } from "./fixtures";
|
||
import HistoryView from "./HistoryView";
|
||
import SourcesView from "./SourcesView";
|
||
import AnalyticsView from "./AnalyticsView";
|
||
import { CacheView } from "./CacheView";
|
||
import type { CacheData, HistoryData, SourcesData } from "./mappers";
|
||
import type { Analytics } from "./types";
|
||
|
||
interface SectionOverlayProps {
|
||
active: number;
|
||
onClose: () => void;
|
||
onNavigate: (i: number) => void;
|
||
// Mapped overlay data, fed by page.tsx (mapHistory / mapSources / mapAnalytics
|
||
// / mapCache). Optional so unwired/storybook usage falls back to each view's
|
||
// fixture default.
|
||
history?: HistoryData;
|
||
sources?: SourcesData;
|
||
analytics?: Analytics;
|
||
cache?: CacheData;
|
||
}
|
||
|
||
export default function SectionOverlay({
|
||
active,
|
||
onClose,
|
||
onNavigate,
|
||
history,
|
||
sources,
|
||
analytics,
|
||
cache,
|
||
}: SectionOverlayProps) {
|
||
return (
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
left: 28,
|
||
right: 28,
|
||
top: 80,
|
||
bottom: 30,
|
||
zIndex: 30,
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
background:
|
||
"linear-gradient(180deg,rgba(244,248,252,.985),rgba(235,242,250,.985))",
|
||
backdropFilter: "blur(12px)",
|
||
border: `1px solid ${tokens.line3}`,
|
||
borderRadius: 8,
|
||
boxShadow: "0 22px 60px rgba(40,80,130,.18)",
|
||
overflow: "hidden",
|
||
fontFamily: tokens.font.sans,
|
||
}}
|
||
>
|
||
<style>{`
|
||
.tiv2-so-back { transition: all .15s; }
|
||
.tiv2-so-back:hover { border-color: ${tokens.accent}; color: ${tokens.accent}; }
|
||
`}</style>
|
||
|
||
{/* HUD corner brackets */}
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
left: 12,
|
||
top: 12,
|
||
width: 15,
|
||
height: 15,
|
||
borderLeft: `1.5px solid ${tokens.accent}`,
|
||
borderTop: `1.5px solid ${tokens.accent}`,
|
||
pointerEvents: "none",
|
||
}}
|
||
/>
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
right: 12,
|
||
bottom: 12,
|
||
width: 15,
|
||
height: 15,
|
||
borderRight: `1.5px solid ${tokens.accent}`,
|
||
borderBottom: `1.5px solid ${tokens.accent}`,
|
||
pointerEvents: "none",
|
||
}}
|
||
/>
|
||
|
||
{/* overlay header */}
|
||
<div
|
||
style={{
|
||
flex: "0 0 auto",
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "space-between",
|
||
padding: "16px 24px",
|
||
borderBottom: "1px solid #cdd9e6",
|
||
}}
|
||
>
|
||
<div style={{ display: "flex", alignItems: "baseline", gap: 11 }}>
|
||
<span
|
||
style={{
|
||
fontFamily: tokens.font.mono,
|
||
fontSize: 15,
|
||
color: tokens.accent,
|
||
}}
|
||
>
|
||
{overlayNums[active]}
|
||
</span>
|
||
<span
|
||
style={{
|
||
fontSize: 14,
|
||
fontWeight: 600,
|
||
letterSpacing: 2.5,
|
||
color: tokens.ink2,
|
||
}}
|
||
>
|
||
{overlayTitles[active]}
|
||
</span>
|
||
</div>
|
||
<div
|
||
onClick={onClose}
|
||
className="tiv2-so-back"
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 8,
|
||
cursor: "pointer",
|
||
border: `1px solid ${tokens.line}`,
|
||
borderRadius: 6,
|
||
padding: "6px 12px",
|
||
background: tokens.surface.w60,
|
||
}}
|
||
>
|
||
<span style={{ fontSize: 13 }}>←</span>
|
||
<span
|
||
style={{ fontSize: 10, letterSpacing: 1.5, color: tokens.muted }}
|
||
>
|
||
К ОЦЕНКЕ
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* overlay body */}
|
||
<div
|
||
style={{
|
||
flex: 1,
|
||
minHeight: 0,
|
||
overflowY: "auto",
|
||
padding: "22px 24px 26px",
|
||
}}
|
||
>
|
||
{active === 1 && <HistoryView data={history} />}
|
||
{active === 2 && <SourcesView data={sources} />}
|
||
{active === 3 && (
|
||
<AnalyticsView data={analytics} onNavigate={onNavigate} />
|
||
)}
|
||
{active === 4 && <CacheView data={cache} />}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|