Browser audit of the live /trade-in/v2 found honesty + UX defects; fixes: - 🔴 LocationDrawer: removed FABRICATED coef 0.87 / fake 'base×coef=result' / invented POI factors / false 'OpenStreetMap' source. Now honest static 'ПОЯСНЕНИЕ К РАСЧЁТУ' (real methodology) + 'коэф. локации в разработке' note. + Escape-to-close. - 🔴 Map (ParamsPanel): removed static fixture price markers; renders REAL analog markers via new mapMarkers(estimate) (projects lat/lon around target, cap 6). - ParamsPanel: empty площадь/address now shows a validation message instead of silently blocking submit; numeric placeholders made clearly placeholders ('напр. 54'). - HeroBar: dead 'КАК РАССЧИТАНО' button now opens the info drawer; building.png next/image 400 fixed (unoptimized + onError fallback — basePath wasn't applied to the optimizer URL). - TopNav: real user from useMe() (name/initials/org/email) instead of fixture 'Андрей Петров'; dead Профиль/Настройки/Помощь dimmed/disabled; Выйти → logout(). - mappers: de-glue analog addresses ('69/2Геологическая'→'69/2 Геологическая', preserves house letters like 14А); 'Продажи в доме'→'Продажи рядом' (radius data); торг sign fixed (backend (start−last)/start positive=reduction → renders '−3.3%'). - HistoryView: '0 ЛОТА' → correct plural via pluralRu. next build green; code-reviewer ✅ APPROVE.
236 lines
7.1 KiB
TypeScript
236 lines
7.1 KiB
TypeScript
"use client";
|
||
|
||
// "ПОЯСНЕНИЕ К РАСЧЁТУ" right-side drawer for the /trade-in/v2 "МЕРА Оценка"
|
||
// design port. Opened from HeroBar (the "?" near "КОЭФ. ЛОКАЦИИ"). This used to
|
||
// render a FABRICATED location coefficient (0.87), a fake "base × coef = result"
|
||
// formula and invented POI factor lists with a false "Источник: OpenStreetMap"
|
||
// footer — none of which the backend produces (location-coef is deferred,
|
||
// backend #2045). It is now an HONEST methodology panel with STATIC, truthful
|
||
// documentation text only: how the estimate is built, and an explicit note that
|
||
// the location coefficient does not exist yet. Keeps the same drawer shell /
|
||
// slide animation / close button. Open/close is driven entirely by props; the
|
||
// only effect is an Escape-to-close keydown listener while open.
|
||
|
||
import { useEffect } from "react";
|
||
import { tokens } from "./tokens";
|
||
|
||
interface LocationDrawerProps {
|
||
open: boolean;
|
||
onClose: () => void;
|
||
}
|
||
|
||
export function LocationDrawer({ open, onClose }: LocationDrawerProps) {
|
||
// Escape-to-close (UI effect only — no HTTP). Active while the drawer is open.
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
const onKey = (e: KeyboardEvent) => {
|
||
if (e.key === "Escape") onClose();
|
||
};
|
||
window.addEventListener("keydown", onKey);
|
||
return () => window.removeEventListener("keydown", onKey);
|
||
}, [open, onClose]);
|
||
|
||
return (
|
||
<>
|
||
<style>{`
|
||
.ld-close {
|
||
width: 32px;
|
||
height: 32px;
|
||
flex: 0 0 auto;
|
||
border: 1px solid ${tokens.line};
|
||
border-radius: 6px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
color: ${tokens.muted};
|
||
font-size: 16px;
|
||
background: ${tokens.surface.w60};
|
||
transition: all .15s;
|
||
}
|
||
.ld-close:hover {
|
||
border-color: ${tokens.accent};
|
||
color: ${tokens.accent};
|
||
}
|
||
`}</style>
|
||
|
||
{/* scrim */}
|
||
<div
|
||
onClick={onClose}
|
||
style={{
|
||
position: "absolute",
|
||
inset: 0,
|
||
background: "rgba(22,42,70,.32)",
|
||
backdropFilter: "blur(2px)",
|
||
opacity: open ? 1 : 0,
|
||
pointerEvents: open ? "auto" : "none",
|
||
transition: "opacity .3s",
|
||
zIndex: 40,
|
||
}}
|
||
/>
|
||
|
||
{/* drawer */}
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
top: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
width: 452,
|
||
transform: open ? "translateX(0)" : "translateX(108%)",
|
||
transition: "transform .38s cubic-bezier(.22,.61,.36,1)",
|
||
zIndex: 41,
|
||
background:
|
||
"linear-gradient(180deg,rgba(245,249,253,.97),rgba(235,242,250,.97))",
|
||
backdropFilter: "blur(10px)",
|
||
borderLeft: `1px solid ${tokens.line3}`,
|
||
boxShadow: "-18px 0 50px rgba(40,80,130,.16)",
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
padding: "30px 32px",
|
||
overflowY: "auto",
|
||
fontFamily: tokens.font.sans,
|
||
}}
|
||
>
|
||
{/* left accent rail */}
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
left: 0,
|
||
top: 18,
|
||
bottom: 18,
|
||
width: 1,
|
||
background: `linear-gradient(180deg,transparent,${tokens.accent},transparent)`,
|
||
opacity: 0.5,
|
||
}}
|
||
/>
|
||
{/* corner brackets */}
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
left: 14,
|
||
top: 14,
|
||
width: 16,
|
||
height: 16,
|
||
borderLeft: `1.5px solid ${tokens.accent}`,
|
||
borderTop: `1.5px solid ${tokens.accent}`,
|
||
}}
|
||
/>
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
right: 14,
|
||
bottom: 14,
|
||
width: 16,
|
||
height: 16,
|
||
borderRight: `1.5px solid ${tokens.accent}`,
|
||
borderBottom: `1.5px solid ${tokens.accent}`,
|
||
}}
|
||
/>
|
||
|
||
{/* header */}
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "flex-start",
|
||
justifyContent: "space-between",
|
||
}}
|
||
>
|
||
<div>
|
||
<div
|
||
style={{
|
||
fontSize: 9,
|
||
letterSpacing: "3px",
|
||
color: tokens.muted2,
|
||
}}
|
||
>
|
||
МЕТОДИКА
|
||
</div>
|
||
<div
|
||
style={{
|
||
fontSize: 22,
|
||
fontWeight: 300,
|
||
letterSpacing: "1.5px",
|
||
color: tokens.ink2,
|
||
marginTop: 8,
|
||
}}
|
||
>
|
||
ПОЯСНЕНИЕ К РАСЧЁТУ
|
||
</div>
|
||
</div>
|
||
<div className="ld-close" onClick={onClose}>
|
||
✕
|
||
</div>
|
||
</div>
|
||
|
||
{/* divider under header */}
|
||
<div
|
||
style={{
|
||
marginTop: 22,
|
||
borderBottom: `1px solid ${tokens.lineSoft}`,
|
||
}}
|
||
/>
|
||
|
||
{/* methodology section */}
|
||
<div
|
||
style={{
|
||
fontSize: 9,
|
||
letterSpacing: "2px",
|
||
color: tokens.muted2,
|
||
marginTop: 22,
|
||
marginBottom: 12,
|
||
}}
|
||
>
|
||
КАК СЧИТАЕМ
|
||
</div>
|
||
<div
|
||
style={{
|
||
fontSize: 12.5,
|
||
lineHeight: 1.7,
|
||
color: tokens.body2,
|
||
}}
|
||
>
|
||
Агрегируем объявления (Авито, Циан, Яндекс, Домклик) и сделки
|
||
Росреестра по сопоставимым квартирам. Медиана{" "}
|
||
<b style={{ color: tokens.ink2 }}>₽/м²</b> → 3 оценки:{" "}
|
||
<b style={{ color: tokens.ink2 }}>
|
||
рекомендованная цена в объявлении
|
||
</b>{" "}
|
||
(asking),{" "}
|
||
<b style={{ color: tokens.ink2 }}>ожидаемая цена сделки</b> (asking −
|
||
торг по ДКП),{" "}
|
||
<b style={{ color: tokens.ink2 }}>ДКП·Росреестр</b> (фактические
|
||
сделки). CV — коэффициент вариации выборки (разброс цен).
|
||
</div>
|
||
|
||
{/* location note section */}
|
||
<div
|
||
style={{
|
||
fontSize: 9,
|
||
letterSpacing: "2px",
|
||
color: tokens.muted2,
|
||
marginTop: 26,
|
||
marginBottom: 12,
|
||
}}
|
||
>
|
||
ЛОКАЦИЯ
|
||
</div>
|
||
<div
|
||
style={{
|
||
fontSize: 12.5,
|
||
lineHeight: 1.7,
|
||
color: tokens.body2,
|
||
background: tokens.infoSoftBg,
|
||
border: `1px solid ${tokens.infoSoftBorder}`,
|
||
borderRadius: 7,
|
||
padding: "12px 14px",
|
||
}}
|
||
>
|
||
Коэффициент локации (POI, транспорт, шум) —{" "}
|
||
<b style={{ color: tokens.ink2 }}>в разработке</b>; текущая оценка по
|
||
локации не корректируется.
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|