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.
451 lines
14 KiB
TypeScript
451 lines
14 KiB
TypeScript
"use client";
|
||
|
||
// Top navigation bar for the /trade-in/v2 "МЕРА Оценка" design port.
|
||
// Faithful markup port of the design header (МЕРА Оценка.dc.html, lines 42-90):
|
||
// inline SVG logo + version + 5 nav tabs (active underline/triangle) + user menu.
|
||
// Tabs change only local UI state via onNavigate; the user dropdown owns its
|
||
// own useState. No data fetching — labels/version come from fixtures, the user
|
||
// identity is fed in from the page (real useMe), colours from tokens.
|
||
|
||
import { useState } from "react";
|
||
import type { CSSProperties } from "react";
|
||
|
||
import { tokens } from "./tokens";
|
||
import { navLabels, version } from "./fixtures";
|
||
|
||
// Real logged-in user identity, derived by the page from useMe()
|
||
// ({username, role, brand}). Deliberately excludes `reports` — the «Мои
|
||
// отчёты» badge count is a separate prop fed from useQuota().used.
|
||
interface TopNavUser {
|
||
name: string;
|
||
org: string;
|
||
email: string;
|
||
initials: string;
|
||
}
|
||
|
||
interface TopNavProps {
|
||
active: number;
|
||
onNavigate: (i: number) => void;
|
||
// «Мои отчёты» badge count — page feeds it from useQuota().used (per-user
|
||
// estimate count). Defaults to 0 for unwired usage.
|
||
reports?: number;
|
||
// Real logged-in user. undefined → neutral «Гость» placeholder, NEVER the
|
||
// old design fixture ("Андрей Петров / Брусника").
|
||
user?: TopNavUser;
|
||
// Sign out — page wires this to logout(); closes the menu first.
|
||
onLogout?: () => void;
|
||
}
|
||
|
||
// Neutral fallback when the user prop is absent (loading / unauthenticated).
|
||
const GUEST_USER: TopNavUser = {
|
||
name: "Гость",
|
||
org: "—",
|
||
email: "—",
|
||
initials: "—",
|
||
};
|
||
|
||
const menuItemStyle: CSSProperties = {
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: "10px",
|
||
padding: "9px 10px",
|
||
borderRadius: "6px",
|
||
cursor: "pointer",
|
||
fontSize: "12.5px",
|
||
color: tokens.body,
|
||
transition: "background .12s",
|
||
};
|
||
|
||
// Профиль / Настройки / Помощь have no pages yet — render them dimmed and
|
||
// non-interactive (no hover class, default cursor) so they read as disabled.
|
||
const menuItemDisabledStyle: CSSProperties = {
|
||
...menuItemStyle,
|
||
cursor: "default",
|
||
color: tokens.muted2,
|
||
};
|
||
|
||
export default function TopNav({
|
||
active,
|
||
onNavigate,
|
||
reports = 0,
|
||
user,
|
||
onLogout,
|
||
}: TopNavProps) {
|
||
const [userOpen, setUserOpen] = useState(false);
|
||
const u = user ?? GUEST_USER;
|
||
|
||
return (
|
||
<>
|
||
<style>{`
|
||
.tnav-tab { color: ${tokens.muted}; font-weight: 400; cursor: pointer; transition: color .2s; }
|
||
.tnav-tab:hover { color: ${tokens.ink}; }
|
||
.tnav-tab--active { color: ${tokens.ink}; font-weight: 600; }
|
||
.tnav-userbtn:hover { background: rgba(46,139,255,.07); }
|
||
.tnav-menuitem:hover { background: rgba(46,139,255,.09); }
|
||
.tnav-logout:hover { background: rgba(205,104,104,.1); }
|
||
`}</style>
|
||
|
||
<div
|
||
style={{
|
||
height: "54px",
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "space-between",
|
||
borderBottom: "1px solid rgba(150,175,200,.4)",
|
||
flex: "0 0 auto",
|
||
}}
|
||
>
|
||
{/* Logo + version */}
|
||
<div style={{ display: "flex", alignItems: "center", gap: "13px" }}>
|
||
<svg
|
||
width="184"
|
||
height="53"
|
||
viewBox="100 440 812 238"
|
||
fill="none"
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
>
|
||
<g transform="translate(0,-20)">
|
||
<path
|
||
d="M164 520h250M610 520h250"
|
||
stroke="#c2d3e3"
|
||
strokeWidth="2.5"
|
||
fill="none"
|
||
/>
|
||
<path
|
||
d="M436 520h78"
|
||
stroke="#2e8bff"
|
||
strokeWidth="4"
|
||
fill="none"
|
||
/>
|
||
<circle cx="548" cy="520" r="5" fill="#2e8bff" />
|
||
<circle cx="790" cy="520" r="5" fill="#2e8bff" opacity=".75" />
|
||
<path
|
||
d="M118 502v36M906 502v36"
|
||
stroke="#c2d3e3"
|
||
strokeWidth="2"
|
||
fill="none"
|
||
opacity=".7"
|
||
/>
|
||
<path
|
||
d="M112 520h22M890 520h22"
|
||
stroke="#2e8bff"
|
||
strokeWidth="3"
|
||
fill="none"
|
||
opacity=".75"
|
||
/>
|
||
<text
|
||
x="512"
|
||
y="572"
|
||
textAnchor="middle"
|
||
style={{ fontFamily: tokens.font.sans }}
|
||
fontSize="150"
|
||
fontWeight="300"
|
||
letterSpacing="16"
|
||
fill="#17263a"
|
||
>
|
||
МЕРА
|
||
</text>
|
||
<text
|
||
x="512"
|
||
y="672"
|
||
textAnchor="middle"
|
||
style={{ fontFamily: tokens.font.sans }}
|
||
fontSize="27"
|
||
fontWeight="400"
|
||
letterSpacing="11"
|
||
fill="#6f8195"
|
||
>
|
||
ОЦЕНКА НЕДВИЖИМОСТИ
|
||
</text>
|
||
</g>
|
||
</svg>
|
||
<div
|
||
style={{
|
||
fontFamily: tokens.font.mono,
|
||
fontSize: "9.5px",
|
||
letterSpacing: "1px",
|
||
color: tokens.muted2,
|
||
borderLeft: `1px solid ${tokens.line}`,
|
||
paddingLeft: "12px",
|
||
}}
|
||
>
|
||
{version}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Nav tabs */}
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: "24px",
|
||
fontSize: "11px",
|
||
letterSpacing: "1px",
|
||
}}
|
||
>
|
||
{navLabels.map((label, i) => (
|
||
<div
|
||
key={label}
|
||
onClick={() => onNavigate(i)}
|
||
className={`tnav-tab${active === i ? " tnav-tab--active" : ""}`}
|
||
style={{ position: "relative", whiteSpace: "nowrap" }}
|
||
>
|
||
{label}
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
left: "50%",
|
||
transform: "translateX(-50%)",
|
||
bottom: "-16px",
|
||
width: "30px",
|
||
height: "2px",
|
||
background: tokens.accent,
|
||
opacity: active === i ? 1 : 0,
|
||
transition: "opacity .2s",
|
||
}}
|
||
/>
|
||
{i === 0 && (
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
left: "50%",
|
||
transform: "translateX(-50%)",
|
||
bottom: "-22px",
|
||
width: 0,
|
||
height: 0,
|
||
borderLeft: "3px solid transparent",
|
||
borderRight: "3px solid transparent",
|
||
borderTop: `4px solid ${tokens.accent}`,
|
||
opacity: active === 0 ? 1 : 0,
|
||
}}
|
||
/>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* User menu */}
|
||
<div style={{ position: "relative" }}>
|
||
<div
|
||
onClick={() => setUserOpen((o) => !o)}
|
||
className="tnav-userbtn"
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: "10px",
|
||
cursor: "pointer",
|
||
padding: "5px 8px 5px 6px",
|
||
borderRadius: "9px",
|
||
transition: "background .15s",
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
width: "34px",
|
||
height: "34px",
|
||
borderRadius: "50%",
|
||
background: `linear-gradient(135deg, ${tokens.accentLite}, ${tokens.accentDeep})`,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
color: tokens.onAccent,
|
||
fontSize: "12px",
|
||
fontWeight: 600,
|
||
fontFamily: tokens.font.sans,
|
||
letterSpacing: ".5px",
|
||
boxShadow: "0 3px 10px rgba(46,139,255,.3)",
|
||
}}
|
||
>
|
||
{u.initials}
|
||
</div>
|
||
<div style={{ lineHeight: 1.25 }}>
|
||
<div
|
||
style={{
|
||
fontSize: "12.5px",
|
||
fontWeight: 600,
|
||
color: tokens.ink,
|
||
fontFamily: tokens.font.sans,
|
||
}}
|
||
>
|
||
{u.name}
|
||
</div>
|
||
<div
|
||
style={{
|
||
fontSize: "9px",
|
||
color: tokens.muted2,
|
||
fontFamily: tokens.font.sans,
|
||
letterSpacing: ".5px",
|
||
}}
|
||
>
|
||
{u.org}
|
||
</div>
|
||
</div>
|
||
<span style={{ color: tokens.muted2, fontSize: "9px" }}>▾</span>
|
||
</div>
|
||
|
||
{userOpen && (
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
right: 0,
|
||
top: "calc(100% + 9px)",
|
||
width: "236px",
|
||
background: tokens.surface.w99,
|
||
border: `1px solid ${tokens.line}`,
|
||
borderRadius: "9px",
|
||
boxShadow: "0 16px 40px rgba(40,80,130,.2)",
|
||
zIndex: 50,
|
||
padding: "7px",
|
||
fontFamily: tokens.font.sans,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: "11px",
|
||
padding: "10px 10px 12px",
|
||
borderBottom: `1px solid ${tokens.lineSoft2}`,
|
||
marginBottom: "6px",
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
width: "38px",
|
||
height: "38px",
|
||
borderRadius: "50%",
|
||
background: `linear-gradient(135deg, ${tokens.accentLite}, ${tokens.accentDeep})`,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
color: tokens.onAccent,
|
||
fontSize: "13px",
|
||
fontWeight: 600,
|
||
}}
|
||
>
|
||
{u.initials}
|
||
</div>
|
||
<div>
|
||
<div
|
||
style={{
|
||
fontSize: "13px",
|
||
fontWeight: 600,
|
||
color: tokens.ink2,
|
||
}}
|
||
>
|
||
{u.name}
|
||
</div>
|
||
<div style={{ fontSize: "10px", color: tokens.muted2 }}>
|
||
{u.email}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={menuItemDisabledStyle} aria-disabled="true">
|
||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none">
|
||
<circle
|
||
cx="7.5"
|
||
cy="5"
|
||
r="3"
|
||
stroke="#6f8195"
|
||
strokeWidth="1.2"
|
||
/>
|
||
<path
|
||
d="M2 13c0-3 2.5-4.5 5.5-4.5S13 10 13 13"
|
||
stroke="#6f8195"
|
||
strokeWidth="1.2"
|
||
/>
|
||
</svg>
|
||
Профиль
|
||
</div>
|
||
|
||
<div className="tnav-menuitem" style={menuItemStyle}>
|
||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none">
|
||
<path
|
||
d="M3 1.5h6l3 3v9H3z"
|
||
stroke="#6f8195"
|
||
strokeWidth="1.2"
|
||
/>
|
||
<path d="M9 1.5v3h3" stroke="#6f8195" strokeWidth="1.2" />
|
||
</svg>
|
||
Мои отчёты{" "}
|
||
<span
|
||
style={{
|
||
marginLeft: "auto",
|
||
fontFamily: tokens.font.mono,
|
||
fontSize: "10px",
|
||
color: tokens.accent,
|
||
}}
|
||
>
|
||
{reports}
|
||
</span>
|
||
</div>
|
||
|
||
<div style={menuItemDisabledStyle} aria-disabled="true">
|
||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none">
|
||
<circle
|
||
cx="7.5"
|
||
cy="7.5"
|
||
r="2.3"
|
||
stroke="#6f8195"
|
||
strokeWidth="1.2"
|
||
/>
|
||
<path
|
||
d="M7.5 1v2M7.5 12v2M1 7.5h2M12 7.5h2"
|
||
stroke="#6f8195"
|
||
strokeWidth="1.2"
|
||
/>
|
||
</svg>
|
||
Настройки
|
||
</div>
|
||
|
||
<div style={menuItemDisabledStyle} aria-disabled="true">
|
||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none">
|
||
<circle
|
||
cx="7.5"
|
||
cy="7.5"
|
||
r="6"
|
||
stroke="#6f8195"
|
||
strokeWidth="1.2"
|
||
/>
|
||
<path
|
||
d="M5.8 5.8a1.7 1.7 0 1 1 2.4 2.1c-.5.4-.7.7-.7 1.3M7.5 11.2v.2"
|
||
stroke="#6f8195"
|
||
strokeWidth="1.2"
|
||
/>
|
||
</svg>
|
||
Помощь
|
||
</div>
|
||
|
||
<div
|
||
style={{
|
||
height: "1px",
|
||
background: tokens.lineSoft2,
|
||
margin: "6px 4px",
|
||
}}
|
||
/>
|
||
|
||
<div
|
||
onClick={() => {
|
||
setUserOpen(false);
|
||
onLogout?.();
|
||
}}
|
||
className="tnav-logout"
|
||
style={{ ...menuItemStyle, color: tokens.danger }}
|
||
>
|
||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none">
|
||
<path
|
||
d="M9 1.5H2.5v12H9M6 7.5h7M10.5 5l2.5 2.5L10.5 10"
|
||
stroke="#cd6868"
|
||
strokeWidth="1.2"
|
||
/>
|
||
</svg>
|
||
Выйти
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|