"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; 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, 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}
ДЕЙСТВИТЕЛЕН ДО
{data.report.validUntil}
{pdfHref ? ( {pdfBtnInner} ) : ( )}
{/* 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}
КОЭФ. ЛОКАЦИИ {data.object.locationCoef === "—" ? ( скоро ) : ( {data.object.locationCoef} )} ?
{/* compass */}
{data.object.compass}
{/* distance scale */}
0 25 50 75 100м
{/* corner brackets */}
); }