fix(tradein/ui): мини-карта адреса вместо статичного фото чужого дома в шапке #2529
5 changed files with 196 additions and 145 deletions
|
|
@ -135,8 +135,8 @@ const EMPTY_OBJECT: ObjectInfo = {
|
||||||
repair: "—",
|
repair: "—",
|
||||||
balcony: false,
|
balcony: false,
|
||||||
locationCoef: "—",
|
locationCoef: "—",
|
||||||
streetView: "",
|
lat: null,
|
||||||
compass: "",
|
lon: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Skeleton pulse (opacity only — NO shimmer sweep, per .claude/rules/ui-*) +
|
// Skeleton pulse (opacity only — NO shimmer sweep, per .claude/rules/ui-*) +
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, type CSSProperties } from "react";
|
import { useEffect, useRef, useState, type CSSProperties } from "react";
|
||||||
|
|
||||||
import { API_BASE_URL } from "@/lib/api";
|
import { API_BASE_URL } from "@/lib/api";
|
||||||
import { safeUrl } from "@/lib/safeUrl";
|
import { safeUrl } from "@/lib/safeUrl";
|
||||||
|
|
@ -12,11 +12,6 @@ import type { HeroBarData } from "./mappers";
|
||||||
// Default presentation data (unwired usage): the existing design fixtures.
|
// Default presentation data (unwired usage): the existing design fixtures.
|
||||||
const HERO_FIXTURE: HeroBarData = { report, object };
|
const HERO_FIXTURE: HeroBarData = { report, object };
|
||||||
|
|
||||||
// next/image does NOT prepend the configured basePath ("/trade-in") to a
|
|
||||||
// literal src, so an <Image src="/trade-in-v2/…"> 404s behind Caddy. A plain
|
|
||||||
// <img> 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
|
// estimate ids are server-issued UUIDs — reject anything else before it lands
|
||||||
// in the PDF request path so a tampered id cannot be injected.
|
// in the PDF request path so a tampered id cannot be injected.
|
||||||
const PDF_UUID_RE =
|
const PDF_UUID_RE =
|
||||||
|
|
@ -37,6 +32,157 @@ function pdfDownloadHref(estimateId: string | null | undefined): string | null {
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Locator mini-map (Leaflet + OSM) ────────────────────────────────────────
|
||||||
|
// Replaces the old static building.png stock photo — user-reported bug: that
|
||||||
|
// single asset was shown for EVERY estimate regardless of the real address,
|
||||||
|
// misleading users into thinking they were looking at their own building.
|
||||||
|
// PORTS the Leaflet-CDN loader pattern from ./SourcesMap.tsx (itself ported
|
||||||
|
// from the dead v1 tree's MapCard.tsx) — copied rather than imported so this
|
||||||
|
// file stays a self-contained port with no shared runtime module and no npm
|
||||||
|
// Leaflet dep, same rationale as SourcesMap.
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any -- интероп с CDN-библиотекой Leaflet (см. SourcesMap.tsx) */
|
||||||
|
const LEAFLET_VER = "1.9.4";
|
||||||
|
const LEAFLET_CSS = `https://unpkg.com/leaflet@${LEAFLET_VER}/dist/leaflet.css`;
|
||||||
|
const LEAFLET_JS = `https://unpkg.com/leaflet@${LEAFLET_VER}/dist/leaflet.js`;
|
||||||
|
const LEAFLET_CSS_SRI = "sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=";
|
||||||
|
const LEAFLET_JS_SRI = "sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=";
|
||||||
|
|
||||||
|
/** Подгружает Leaflet с CDN один раз, резолвит window.L. (mirror SourcesMap.tsx) */
|
||||||
|
function loadLeaflet(): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const w = window as any;
|
||||||
|
if (w.L) {
|
||||||
|
resolve(w.L);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!document.querySelector(`link[data-leaflet]`)) {
|
||||||
|
const link = document.createElement("link");
|
||||||
|
link.rel = "stylesheet";
|
||||||
|
link.href = LEAFLET_CSS;
|
||||||
|
link.integrity = LEAFLET_CSS_SRI;
|
||||||
|
link.crossOrigin = "anonymous";
|
||||||
|
link.setAttribute("data-leaflet", "1");
|
||||||
|
document.head.appendChild(link);
|
||||||
|
}
|
||||||
|
const existing = document.querySelector<HTMLScriptElement>(`script[data-leaflet]`);
|
||||||
|
if (existing) {
|
||||||
|
existing.addEventListener("load", () => resolve(w.L));
|
||||||
|
existing.addEventListener("error", () => reject(new Error("leaflet load failed")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = LEAFLET_JS;
|
||||||
|
script.integrity = LEAFLET_JS_SRI;
|
||||||
|
script.crossOrigin = "anonymous";
|
||||||
|
script.setAttribute("data-leaflet", "1");
|
||||||
|
script.onload = () => resolve(w.L);
|
||||||
|
script.onerror = () => reject(new Error("leaflet load failed"));
|
||||||
|
document.body.appendChild(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overview zoom: close enough to recognise the actual building on a 560×152
|
||||||
|
// box without feeling zoomed-in on bare rooftops (SourcesMap's multi-pin
|
||||||
|
// overlay map fits bounds instead — this is a single-point locator, not an
|
||||||
|
// exploration map).
|
||||||
|
const HERO_MAP_ZOOM = 16;
|
||||||
|
|
||||||
|
interface HeroMiniMapProps {
|
||||||
|
lat: number | null;
|
||||||
|
lon: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Small OSM/Leaflet locator map centred on the subject address, replacing the
|
||||||
|
* old static building.png photo. Deliberately near-non-interactive — this is
|
||||||
|
* a 560×152 "you are here" badge, not an explorable map (ParamsPanel/
|
||||||
|
* SourcesMap already cover that): dragging/zoomControl/scroll-zoom/dbl-click
|
||||||
|
* zoom are all off so the box reads as a locator, not a broken-feeling mini
|
||||||
|
* map, and never steals the page's scroll or click focus.
|
||||||
|
*/
|
||||||
|
function HeroMiniMap({ lat, lon }: HeroMiniMapProps) {
|
||||||
|
const mapRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [mapError, setMapError] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (lat == null || lon == null) return;
|
||||||
|
let map: any = null;
|
||||||
|
let cancelled = false;
|
||||||
|
// Reset a stale failure from a previous address/CDN hiccup before this
|
||||||
|
// attempt — otherwise one bad load latches mapError forever (caught in
|
||||||
|
// review on a sibling PR) and every later estimate shows the placeholder
|
||||||
|
// even once the CDN is reachable again.
|
||||||
|
setMapError(false);
|
||||||
|
|
||||||
|
loadLeaflet()
|
||||||
|
.then((L) => {
|
||||||
|
if (cancelled || !mapRef.current) return;
|
||||||
|
map = L.map(mapRef.current, {
|
||||||
|
scrollWheelZoom: false, // embedded in the page — must not steal page scroll
|
||||||
|
dragging: false, // locator badge, not an explorable map
|
||||||
|
touchZoom: false,
|
||||||
|
doubleClickZoom: false,
|
||||||
|
zoomControl: false, // no room for +/- controls at this size
|
||||||
|
keyboard: false,
|
||||||
|
}).setView([lat, lon], HERO_MAP_ZOOM);
|
||||||
|
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
|
attribution: "© OpenStreetMap",
|
||||||
|
maxZoom: 19,
|
||||||
|
}).addTo(map);
|
||||||
|
L.circleMarker([lat, lon], {
|
||||||
|
radius: 8,
|
||||||
|
color: "#fff",
|
||||||
|
weight: 3,
|
||||||
|
fillColor: tokens.accent,
|
||||||
|
fillOpacity: 1,
|
||||||
|
}).addTo(map);
|
||||||
|
setTimeout(() => map && map.invalidateSize(), 120);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!cancelled) setMapError(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
if (map) map.remove();
|
||||||
|
};
|
||||||
|
}, [lat, lon]);
|
||||||
|
|
||||||
|
// Honest empty states — no coordinates yet (fresh/un-geocoded estimate) or
|
||||||
|
// the CDN failed — never a blank rectangle or a broken-image icon.
|
||||||
|
if (lat == null || lon == null || mapError) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
background: tokens.mapBg,
|
||||||
|
color: tokens.muted,
|
||||||
|
fontSize: 11,
|
||||||
|
textAlign: "center",
|
||||||
|
padding: "0 20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{lat == null || lon == null
|
||||||
|
? "Карта появится после расчёта адреса"
|
||||||
|
: "Не удалось загрузить карту"}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={mapRef}
|
||||||
|
style={{ position: "absolute", inset: 0, background: tokens.mapBg }}
|
||||||
|
aria-label="Расположение объекта на карте"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
interface HeroBarProps {
|
interface HeroBarProps {
|
||||||
data?: HeroBarData;
|
data?: HeroBarData;
|
||||||
estimateId?: string | null;
|
estimateId?: string | null;
|
||||||
|
|
@ -46,10 +192,10 @@ interface HeroBarProps {
|
||||||
hasEstimate: boolean;
|
hasEstimate: boolean;
|
||||||
onOpenInfo: () => void;
|
onOpenInfo: () => void;
|
||||||
// #2275 mobile quick-view: a real fluid layout instead of the fixed-width
|
// #2275 mobile quick-view: a real fluid layout instead of the fixed-width
|
||||||
// desktop one — meta/buttons stack, the decorative building photo (and the
|
// desktop one — meta/buttons stack, the locator mini-map (and the
|
||||||
// address/coef card baked into it) is dropped since it assumes a 560×152 box
|
// address/coef card baked into it) is dropped since it assumes a 560×152 box
|
||||||
// that cannot reflow. «КАК РАССЧИТАНО» still opens the same location-coef
|
// that cannot reflow. «КАК РАССЧИТАНО» still opens the same location-coef
|
||||||
// drawer, so no functionality is lost, only the redundant photo-card copy.
|
// drawer, so no functionality is lost, only the redundant map-card copy.
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,9 +238,6 @@ export default function HeroBar({
|
||||||
// A downloadable report exists ⇔ the estimate is ready ⇒ the PDF button is the
|
// A downloadable report exists ⇔ the estimate is ready ⇒ the PDF button is the
|
||||||
// filled/primary CTA (M4). Otherwise it stays a disabled outline.
|
// filled/primary CTA (M4). Otherwise it stays a disabled outline.
|
||||||
const pdfFilled = Boolean(pdfHref);
|
const pdfFilled = Boolean(pdfHref);
|
||||||
// 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 = (filled: boolean) => {
|
const pdfBtnInner = (filled: boolean) => {
|
||||||
const frame = filled ? "#fff" : "#2e8bff";
|
const frame = filled ? "#fff" : "#2e8bff";
|
||||||
const rule = filled ? "rgba(255,255,255,.75)" : "#6f8195";
|
const rule = filled ? "rgba(255,255,255,.75)" : "#6f8195";
|
||||||
|
|
@ -137,7 +280,6 @@ export default function HeroBar({
|
||||||
.hero-pdf-btn-filled:active { transform: translateY(1px); }
|
.hero-pdf-btn-filled:active { transform: translateY(1px); }
|
||||||
.hero-calc-btn:hover { border-color: ${tokens.accent}; color: ${tokens.accent}; }
|
.hero-calc-btn:hover { border-color: ${tokens.accent}; color: ${tokens.accent}; }
|
||||||
.hero-coef-row:hover { background: rgba(46,139,255,.09); }
|
.hero-coef-row:hover { background: rgba(46,139,255,.09); }
|
||||||
@keyframes hero-scanv { 0% { transform: translateY(-100%); } 100% { transform: translateY(900%); } }
|
|
||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
{/* LEFT: meta + buttons */}
|
{/* LEFT: meta + buttons */}
|
||||||
|
|
@ -295,11 +437,15 @@ export default function HeroBar({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* RIGHT: photo + overlays — dropped in compact mode (#2275): the box is
|
{/* RIGHT: locator mini-map + overlays — dropped in compact mode (#2275):
|
||||||
a fixed 560×152 with several absolutely-positioned children (address
|
the box is a fixed 560×152 with several absolutely-positioned
|
||||||
card, compass, distance scale) pinned to that size, so it cannot
|
children (address card) pinned to that size, so it cannot reflow to
|
||||||
reflow to a phone width. «КАК РАССЧИТАНО» above still opens the same
|
a phone width. «КАК РАССЧИТАНО» above still opens the same
|
||||||
location-coef drawer, so no functionality is lost. */}
|
location-coef drawer, so no functionality is lost.
|
||||||
|
User-reported bug: this used to be a single static building.png
|
||||||
|
photo shown for EVERY estimate regardless of the real address (a
|
||||||
|
user could be looking at someone else's building) — replaced with a
|
||||||
|
real Leaflet/OSM map centred on the subject's own coordinates. */}
|
||||||
{!compact && (
|
{!compact && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -310,65 +456,26 @@ export default function HeroBar({
|
||||||
border: `1px solid ${tokens.line3}`,
|
border: `1px solid ${tokens.line3}`,
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
background: tokens.photoBg,
|
background: tokens.mapBg,
|
||||||
boxShadow: "0 6px 26px rgba(40,80,130,.10)",
|
boxShadow: "0 6px 26px rgba(40,80,130,.10)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!imgFailed && (
|
<HeroMiniMap lat={data.object.lat} lon={data.object.lon} />
|
||||||
<img
|
|
||||||
src={`${BP}/trade-in-v2/building.png`}
|
{/* Left fade so the address card (below) stays legible over busy
|
||||||
alt=""
|
map tiles instead of a floating card with no visual anchor — kept
|
||||||
onError={() => setImgFailed(true)}
|
from the old photo styling, where it served the same purpose.
|
||||||
style={{
|
pointerEvents:none so it never blocks map interaction/attribution
|
||||||
position: "absolute",
|
underneath. The old bottom vignette + scanning HUD sweep line are
|
||||||
inset: 0,
|
dropped: both existed purely to stylise/recede the stock photo and
|
||||||
width: "100%",
|
have no equivalent purpose over a live basemap. */}
|
||||||
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 && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
inset: 0,
|
|
||||||
background: "rgba(46,139,255,.14)",
|
|
||||||
mixBlendMode: "multiply",
|
|
||||||
pointerEvents: "none",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
inset: 0,
|
inset: 0,
|
||||||
background:
|
background:
|
||||||
"linear-gradient(90deg,rgba(238,244,250,.96) 0%,rgba(238,244,250,.5) 22%,transparent 40%)",
|
"linear-gradient(90deg,rgba(238,244,250,.96) 0%,rgba(238,244,250,.5) 22%,transparent 40%)",
|
||||||
}}
|
pointerEvents: "none",
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
inset: 0,
|
|
||||||
background:
|
|
||||||
"linear-gradient(0deg,rgba(230,240,250,.45),transparent 40%)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
top: "34%",
|
|
||||||
height: 1,
|
|
||||||
background:
|
|
||||||
"linear-gradient(90deg,transparent,rgba(46,139,255,.5),transparent)",
|
|
||||||
animation: "hero-scanv 6s linear infinite",
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -406,31 +513,6 @@ export default function HeroBar({
|
||||||
{data.object.city}
|
{data.object.city}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/* streetView — Fix #4 (audit): always "" (TODO BE-3, mappers.ts
|
|
||||||
mapObject), so this slot + its divider are hidden together
|
|
||||||
rather than showing an empty caption row. Leaves the single
|
|
||||||
divider below to separate the address block from the coef row. */}
|
|
||||||
{data.object.streetView && (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: 1,
|
|
||||||
background: tokens.lineSoft,
|
|
||||||
margin: "8px 0",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontFamily: tokens.font.mono,
|
|
||||||
fontSize: 9,
|
|
||||||
color: tokens.muted,
|
|
||||||
lineHeight: 1.55,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{data.object.streetView}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: 1,
|
height: 1,
|
||||||
|
|
@ -528,54 +610,14 @@ export default function HeroBar({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* compass — Fix #4 (audit): compass bearing is always "" (TODO BE-3,
|
|
||||||
mappers.ts mapObject), so the icon+label are hidden rather than
|
|
||||||
showing a compass that never actually points anywhere. Mirrors the
|
|
||||||
locationCoef "нет данных" graceful-fallback pattern already used
|
|
||||||
in this file. */}
|
|
||||||
{data.object.compass && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
right: 16,
|
|
||||||
top: 14,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 2,
|
|
||||||
color: tokens.accent,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="22"
|
|
||||||
height="22"
|
|
||||||
viewBox="0 0 22 22"
|
|
||||||
fill="none"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<circle cx="11" cy="11" r="10" stroke="#2e8bff" strokeWidth="1" />
|
|
||||||
<path d="M11 3 L13 11 L11 9 L9 11 Z" fill="#2e8bff" />
|
|
||||||
</svg>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontFamily: tokens.font.mono,
|
|
||||||
fontSize: 8,
|
|
||||||
letterSpacing: ".5px",
|
|
||||||
color: tokens.muted,
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{data.object.compass}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Fix #3 (audit) — the "0/25/50/75/100м" distance ruler here was a
|
{/* Fix #3 (audit) — the "0/25/50/75/100м" distance ruler here was a
|
||||||
hardcoded tick scale over a generic stock photo with no real
|
hardcoded tick scale over a generic stock photo with no real
|
||||||
measurement behind it (not tied to any actual distance/scale
|
measurement behind it (not tied to any actual distance/scale
|
||||||
value). Removed rather than fabricating a scale. */}
|
value). Removed rather than fabricating a scale. */}
|
||||||
|
|
||||||
{/* corner brackets */}
|
{/* corner brackets — decorative HUD framing only; pointerEvents:none
|
||||||
|
so they never sit on top of the map's own bottom-right OSM
|
||||||
|
attribution link. */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
|
@ -585,6 +627,7 @@ export default function HeroBar({
|
||||||
height: 14,
|
height: 14,
|
||||||
borderLeft: `1.5px solid ${tokens.accent}`,
|
borderLeft: `1.5px solid ${tokens.accent}`,
|
||||||
borderTop: `1.5px solid ${tokens.accent}`,
|
borderTop: `1.5px solid ${tokens.accent}`,
|
||||||
|
pointerEvents: "none",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
|
|
@ -596,6 +639,7 @@ export default function HeroBar({
|
||||||
height: 14,
|
height: 14,
|
||||||
borderRight: `1.5px solid ${tokens.accent}`,
|
borderRight: `1.5px solid ${tokens.accent}`,
|
||||||
borderTop: `1.5px solid ${tokens.accent}`,
|
borderTop: `1.5px solid ${tokens.accent}`,
|
||||||
|
pointerEvents: "none",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
|
|
@ -607,6 +651,7 @@ export default function HeroBar({
|
||||||
height: 14,
|
height: 14,
|
||||||
borderLeft: `1.5px solid ${tokens.accent}`,
|
borderLeft: `1.5px solid ${tokens.accent}`,
|
||||||
borderBottom: `1.5px solid ${tokens.accent}`,
|
borderBottom: `1.5px solid ${tokens.accent}`,
|
||||||
|
pointerEvents: "none",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
|
|
@ -618,6 +663,7 @@ export default function HeroBar({
|
||||||
height: 14,
|
height: 14,
|
||||||
borderRight: `1.5px solid ${tokens.accent}`,
|
borderRight: `1.5px solid ${tokens.accent}`,
|
||||||
borderBottom: `1.5px solid ${tokens.accent}`,
|
borderBottom: `1.5px solid ${tokens.accent}`,
|
||||||
|
pointerEvents: "none",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,10 @@ export const object: ObjectInfo = {
|
||||||
repair: "Хороший",
|
repair: "Хороший",
|
||||||
balcony: true,
|
balcony: true,
|
||||||
locationCoef: "0.87",
|
locationCoef: "0.87",
|
||||||
streetView: "Street View · май 2024",
|
// Approximate центр Екатеринбурга near ул. Малышева, 30 — illustrative
|
||||||
compass: "СЕВЕРО-ЗАПАД",
|
// fixture coordinate for the HeroBar locator mini-map (unwired usage only).
|
||||||
|
lat: 56.8384,
|
||||||
|
lon: 60.6057,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- 02 RESULT ------------------------------------------------------------
|
// ---- 02 RESULT ------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,6 @@
|
||||||
// (parseAddress). Backend should return structured address components.
|
// (parseAddress). Backend should return structured address components.
|
||||||
// BE-3 location coefficient shipped 2026-07-03 (#2045) and is wired here
|
// BE-3 location coefficient shipped 2026-07-03 (#2045) and is wired here
|
||||||
// (mapObject/mapLocation consume GET /trade-in/location-coef, #2317).
|
// (mapObject/mapLocation consume GET /trade-in/location-coef, #2317).
|
||||||
// street-view caption / compass bearing are still not in the API →
|
|
||||||
// remain placeholders.
|
|
||||||
//
|
//
|
||||||
// Enum <-> RU reconciliation (design dropdowns have options with no enum value):
|
// Enum <-> RU reconciliation (design dropdowns have options with no enum value):
|
||||||
// house type: 'Блочный' ⇄ enum 'other' (enum has no dedicated block type)
|
// house type: 'Блочный' ⇄ enum 'other' (enum has no dedicated block type)
|
||||||
|
|
@ -820,8 +818,10 @@ export function mapObject(
|
||||||
repair: e.repair_state ? REPAIR_RU[e.repair_state] : "—",
|
repair: e.repair_state ? REPAIR_RU[e.repair_state] : "—",
|
||||||
balcony: e.has_balcony ?? false,
|
balcony: e.has_balcony ?? false,
|
||||||
locationCoef: coefDeltaLabel(coef),
|
locationCoef: coefDeltaLabel(coef),
|
||||||
streetView: "", // TODO BE-3 (backend does not surface a street-view caption)
|
// Same target_lat/target_lon the ParamsPanel/SourcesMap map pins use —
|
||||||
compass: "", // TODO BE-3 (backend does not surface a compass bearing)
|
// null while the estimate has no geocode yet.
|
||||||
|
lat: e.target_lat,
|
||||||
|
lon: e.target_lon,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,11 @@ export interface ObjectInfo {
|
||||||
repair: string;
|
repair: string;
|
||||||
balcony: boolean;
|
balcony: boolean;
|
||||||
locationCoef: string;
|
locationCoef: string;
|
||||||
streetView: string;
|
// Subject coordinates for the HeroBar locator mini-map (Leaflet/OSM). null
|
||||||
compass: string;
|
// when the estimate has no geocode yet — the map then renders an honest
|
||||||
|
// "нет координат" placeholder instead of an empty/broken box.
|
||||||
|
lat: number | null;
|
||||||
|
lon: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 02 RESULT ------------------------------------------------------------
|
// ---- 02 RESULT ------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue