gendesign/tradein-mvp/frontend/src/components/trade-in/v2/TopNav.tsx
bot-backend 5a2b6d91cd
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
fix(tradein/v2): audit findings — de-fabricate location drawer, real map markers, validation, hero image, real user (#2036)
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.
2026-06-28 17:26:12 +03:00

451 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
</>
);
}