gendesign/tradein-mvp/frontend/src/components/trade-in/v2/TopNav.tsx
lekss361 a0647a53a9
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 2m11s
Deploy Trade-In / deploy (push) Successful in 51s
feat(tradein/ui): окно чата поддержки на сайте вместо ухода в Telegram (#2533)
2026-07-26 20:52:30 +00:00

530 lines
18 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";
import { useSupportChat } from "./SupportChatContext";
// 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.
// «Помощь» used to be in this group too, then linked out to the Telegram
// support bot; it now opens the on-site chat window (SupportChatPanel.tsx via
// SupportChatContext) — it renders as a live item.
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 { openChat } = useSupportChat();
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 { background: none; border: none; }
.tnav-userbtn:hover { background: rgba(46,139,255,.07); }
.tnav-menuitem:hover { background: rgba(46,139,255,.09); }
.tnav-logout { background: none; border: none; }
.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"
aria-hidden="true"
>
<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"
// L7 — stale literal from before the C1 contrast sweep darkened
// the muted scale; token keeps this logo subtitle in sync.
fill={tokens.muted}
>
ОЦЕНКА НЕДВИЖИМОСТИ
</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) => (
<button
type="button"
key={label}
onClick={() => onNavigate(i)}
aria-current={active === i ? "page" : undefined}
className={`tnav-tab${active === i ? " tnav-tab--active" : ""}`}
style={{
position: "relative",
whiteSpace: "nowrap",
background: "none",
border: "none",
padding: 0,
margin: 0,
// NB: family/size/letterSpacing inherit for chrome reset, but do
// NOT set `font`/`color` shorthand — the .tnav-tab(--active)/:hover
// CSS must keep driving tab colour + weight.
fontFamily: "inherit",
fontSize: "inherit",
letterSpacing: "inherit",
cursor: "pointer",
}}
>
{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,
}}
/>
)}
</button>
))}
</div>
{/* User menu */}
<div
style={{ position: "relative" }}
onKeyDown={(e) => {
if (e.key === "Escape") setUserOpen(false);
}}
>
<button
type="button"
onClick={() => setUserOpen((o) => !o)}
className="tnav-userbtn"
aria-haspopup="true"
aria-expanded={userOpen}
style={{
display: "flex",
alignItems: "center",
gap: "10px",
cursor: "pointer",
padding: "5px 8px 5px 6px",
borderRadius: "9px",
transition: "background .15s",
fontFamily: "inherit",
textAlign: "left",
}}
>
<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>
</button>
{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>
{/* L5 — `title` gives the dimmed/non-interactive item a hover
tooltip explaining WHY it's disabled, instead of a silent dead
row (mirrored on Настройки below). */}
<div
style={menuItemDisabledStyle}
aria-disabled="true"
title="Раздел «Профиль» скоро появится"
>
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
<circle
cx="7.5"
cy="5"
r="3"
stroke={tokens.muted}
strokeWidth="1.2"
/>
<path
d="M2 13c0-3 2.5-4.5 5.5-4.5S13 10 13 13"
stroke={tokens.muted}
strokeWidth="1.2"
/>
</svg>
Профиль
</div>
<div className="tnav-menuitem" style={menuItemStyle}>
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
<path
d="M3 1.5h6l3 3v9H3z"
stroke={tokens.muted}
strokeWidth="1.2"
/>
<path d="M9 1.5v3h3" stroke={tokens.muted} strokeWidth="1.2" />
</svg>
Мои отчёты{" "}
<span
style={{
marginLeft: "auto",
fontFamily: tokens.font.mono,
fontSize: "10px",
// accentDeep (not accent): 10px counter text; accent #2e8bff
// on the white menu is ~2.9:1, accentDeep clears AA (#2264 C4).
color: tokens.accentDeep,
}}
>
{reports}
</span>
</div>
<div
style={menuItemDisabledStyle}
aria-disabled="true"
title="Раздел «Настройки» скоро появится"
>
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
<circle
cx="7.5"
cy="7.5"
r="2.3"
stroke={tokens.muted}
strokeWidth="1.2"
/>
<path
d="M7.5 1v2M7.5 12v2M1 7.5h2M12 7.5h2"
stroke={tokens.muted}
strokeWidth="1.2"
/>
</svg>
Настройки
</div>
{/* Live item (issue: two support entry points — the other is
SupportButton.tsx's floating button). Both open the same
chat window (SupportChatContext) — no more external
Telegram navigation from here. Styled like the other live
row above ("Мои отчёты"), not the dimmed Профиль/Настройки
rows. */}
<button
type="button"
className="tnav-menuitem"
style={{
...menuItemStyle,
width: "100%",
border: "none",
background: "none",
fontFamily: "inherit",
textAlign: "left",
}}
aria-label="Открыть чат поддержки МЕРА"
title="Открыть чат поддержки МЕРА"
onClick={() => {
setUserOpen(false);
openChat();
}}
>
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
<circle
cx="7.5"
cy="7.5"
r="6"
stroke={tokens.muted}
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={tokens.muted}
strokeWidth="1.2"
/>
</svg>
Помощь
</button>
<div
style={{
height: "1px",
background: tokens.lineSoft2,
margin: "6px 4px",
}}
/>
<button
type="button"
onClick={() => {
setUserOpen(false);
onLogout?.();
}}
className="tnav-logout"
style={{
...menuItemStyle,
color: tokens.danger,
width: "100%",
fontFamily: "inherit",
textAlign: "left",
}}
>
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
<path
d="M9 1.5H2.5v12H9M6 7.5h7M10.5 5l2.5 2.5L10.5 10"
stroke="#cd6868"
strokeWidth="1.2"
/>
</svg>
Выйти
</button>
</div>
)}
</div>
</div>
</>
);
}