"use client"; import { useState, type CSSProperties } from "react"; import { API_BASE_URL } from "@/lib/api"; import { safeUrl } from "@/lib/safeUrl"; import { tokens } from "./tokens"; import { object, report } from "./fixtures"; import type { HeroBarData } from "./mappers"; // Default presentation data (unwired usage): the existing design fixtures. const HERO_FIXTURE: HeroBarData = { report, object }; // next/image does NOT prepend the configured basePath ("/trade-in") to a // literal src, so an 404s behind Caddy. A plain // with the basePath baked in resolves to /trade-in/trade-in-v2/… → 200. const BP = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; // estimate ids are server-issued UUIDs — reject anything else before it lands // in the PDF request path so a tampered id cannot be injected. const PDF_UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; // Build + validate the PDF download URL for an estimate id. Returns null when // there is no estimate yet (→ disabled button), the id is not a UUID, or the // resolved URL fails the safeUrl scheme allowlist. API_BASE_URL is relative // ("" in dev, "/trade-in" behind Caddy), so resolve against the document origin // purely for the safeUrl http/https check; the keeps the relative href // (the browser resolves it against the current page, like OfferCard's link). function pdfDownloadHref(estimateId: string | null | undefined): string | null { if (!estimateId || !PDF_UUID_RE.test(estimateId)) return null; const href = `${API_BASE_URL}/api/v1/trade-in/estimate/${estimateId}/pdf`; if (typeof window === "undefined") return href; return safeUrl(new URL(href, window.location.origin).toString()) ? href : null; } interface HeroBarProps { data?: HeroBarData; estimateId?: string | null; // M4: when false (no real, sufficient estimate yet) the estimate-dependent // meta/controls — the «ДЕЙСТВИТЕЛЕН ДО» validity line and «КАК РАССЧИТАНО» — // are hidden, mirroring how the PDF control already gates on estimate presence. hasEstimate: boolean; onOpenInfo: () => void; } const pdfBtnStyle: CSSProperties = { flex: 1, height: 50, background: tokens.surface.w55, border: `1px solid ${tokens.line}`, borderRadius: 7, display: "flex", alignItems: "center", gap: 11, padding: "0 16px", cursor: "pointer", fontFamily: tokens.font.sans, color: tokens.ink, textDecoration: "none", transition: "all .15s", }; export default function HeroBar({ data = HERO_FIXTURE, estimateId, hasEstimate, onOpenInfo, }: HeroBarProps) { const pdfHref = pdfDownloadHref(estimateId); // Hide the building photo if the asset 404s/400s so the photoBg fill shows // instead of a broken-image icon. const [imgFailed, setImgFailed] = useState(false); const pdfBtnInner = ( <> СКАЧАТЬ PDF-ОТЧЁТ ); return (
{/* LEFT: meta + buttons */}
ОТЧЁТ
{data.report.id}
ДАТА
{data.report.date}
{hasEstimate && (
ДЕЙСТВИТЕЛЕН ДО
{data.report.validUntil}
)}
{pdfHref ? ( {pdfBtnInner} ) : ( )} {hasEstimate && ( )}
{/* RIGHT: photo + overlays */}
{!imgFailed && ( setImgFailed(true)} style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "contain", objectPosition: "right center", filter: "saturate(.25) brightness(1.06) contrast(.95)", }} /> )} {/* blue duotone tint toward HUD accent — recedes the photo (only over the real image; skip when it 404s so the placeholder stays clean) */} {!imgFailed && (
)}
{/* address card */}
АДРЕС
{data.object.address}
{data.object.city}
{data.object.streetView}
(which, with preflight // off, would need a UA-chrome reset and risk the row's exact box // geometry / negative-margin hover bleed). Opens the methodology drawer. role="button" tabIndex={0} className="hero-coef-row" onClick={onOpenInfo} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onOpenInfo(); } }} aria-label="Пояснение к расчёту коэффициента локации" style={{ display: "flex", alignItems: "center", justifyContent: "space-between", cursor: "pointer", borderRadius: 4, margin: "-3px -5px", padding: "3px 5px", transition: "background .15s", }} > КОЭФ. ЛОКАЦИИ {data.object.locationCoef === "—" ? ( скоро ) : ( {data.object.locationCoef} )}
{/* compass */}
{data.object.compass}
{/* distance scale */}
0 25 50 75 100м
{/* corner brackets */}
); }