feat(tradein/v2): HUD-редизайн страницы оценки (Макс) → /trade-in/v2 (#2035)
This commit is contained in:
parent
ca4637e5c8
commit
fba41c0cce
18 changed files with 6350 additions and 0 deletions
BIN
frontend/public/trade-in-v2/building.png
Normal file
BIN
frontend/public/trade-in-v2/building.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 501 KiB |
46
frontend/src/app/trade-in/v2/layout.tsx
Normal file
46
frontend/src/app/trade-in/v2/layout.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import type { Metadata } from "next";
|
||||
import { IBM_Plex_Mono, Manrope } from "next/font/google";
|
||||
|
||||
import { pageBg } from "@/components/trade-in/v2/tokens";
|
||||
|
||||
// Manrope — primary sans typeface of the МЕРА HUD. next/font is bundled
|
||||
// (no package.json change). Cyrillic + latin so RU labels render correctly.
|
||||
const manrope = Manrope({
|
||||
subsets: ["latin", "cyrillic"],
|
||||
weight: ["200", "300", "400", "500", "600", "700"],
|
||||
variable: "--font-manrope",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
// IBM Plex Mono — cockpit numerals / mono labels.
|
||||
const plexMono = IBM_Plex_Mono({
|
||||
subsets: ["latin", "cyrillic"],
|
||||
weight: ["300", "400", "500"],
|
||||
variable: "--font-plex-mono",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "МЕРА · Оценка v2",
|
||||
};
|
||||
|
||||
export default function TradeInV2Layout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`${manrope.variable} ${plexMono.variable}`}
|
||||
style={{
|
||||
minHeight: "100vh",
|
||||
background: pageBg,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "flex-start",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
148
frontend/src/app/trade-in/v2/page.tsx
Normal file
148
frontend/src/app/trade-in/v2/page.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
import { tokens } from "@/components/trade-in/v2/tokens";
|
||||
import TopNav from "@/components/trade-in/v2/TopNav";
|
||||
import HeroBar from "@/components/trade-in/v2/HeroBar";
|
||||
import ParamsPanel from "@/components/trade-in/v2/ParamsPanel";
|
||||
import ResultPanel from "@/components/trade-in/v2/ResultPanel";
|
||||
import { ObjectSummary } from "@/components/trade-in/v2/ObjectSummary";
|
||||
import { Footer } from "@/components/trade-in/v2/Footer";
|
||||
import SectionOverlay from "@/components/trade-in/v2/SectionOverlay";
|
||||
import { LocationDrawer } from "@/components/trade-in/v2/LocationDrawer";
|
||||
|
||||
// OUTER HUD FRAME + 4 corner brackets (design lines 31-37). Decorative,
|
||||
// non-interactive overlay drawn over the artboard gradient. The frame has
|
||||
// chamfered octagonal corners (clip-path polygon) + 4px radius; the brackets
|
||||
// sit just outside it at the artboard corners.
|
||||
const FRAME_INSET = 14;
|
||||
const BRACKET_INSET = 6;
|
||||
const BRACKET_ARM = 22;
|
||||
const BRACKET_W = 1.5;
|
||||
|
||||
const FRAME_CLIP =
|
||||
"polygon(18px 0,calc(100% - 18px) 0,100% 18px,100% calc(100% - 18px),calc(100% - 18px) 100%,18px 100%,0 calc(100% - 18px),0 18px)";
|
||||
|
||||
const brackets: { key: string; style: CSSProperties }[] = [
|
||||
{
|
||||
key: "tl",
|
||||
style: {
|
||||
top: BRACKET_INSET,
|
||||
left: BRACKET_INSET,
|
||||
borderTop: `${BRACKET_W}px solid ${tokens.bracket2}`,
|
||||
borderLeft: `${BRACKET_W}px solid ${tokens.bracket2}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "tr",
|
||||
style: {
|
||||
top: BRACKET_INSET,
|
||||
right: BRACKET_INSET,
|
||||
borderTop: `${BRACKET_W}px solid ${tokens.bracket2}`,
|
||||
borderRight: `${BRACKET_W}px solid ${tokens.bracket2}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "bl",
|
||||
style: {
|
||||
bottom: BRACKET_INSET,
|
||||
left: BRACKET_INSET,
|
||||
borderBottom: `${BRACKET_W}px solid ${tokens.bracket2}`,
|
||||
borderLeft: `${BRACKET_W}px solid ${tokens.bracket2}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "br",
|
||||
style: {
|
||||
bottom: BRACKET_INSET,
|
||||
right: BRACKET_INSET,
|
||||
borderBottom: `${BRACKET_W}px solid ${tokens.bracket2}`,
|
||||
borderRight: `${BRACKET_W}px solid ${tokens.bracket2}`,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default function TradeInV2Page() {
|
||||
const [nav, setNav] = useState(0);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
width: 1536,
|
||||
height: 1024,
|
||||
overflow: "hidden",
|
||||
background: tokens.gradientBg,
|
||||
fontFamily: tokens.font.sans,
|
||||
color: tokens.ink,
|
||||
}}
|
||||
>
|
||||
{/* OUTER HUD FRAME */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: FRAME_INSET,
|
||||
border: `1px solid ${tokens.line}`,
|
||||
borderRadius: 4,
|
||||
clipPath: FRAME_CLIP,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
{/* 4 corner brackets */}
|
||||
{brackets.map((b) => (
|
||||
<div
|
||||
key={b.key}
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: BRACKET_ARM,
|
||||
height: BRACKET_ARM,
|
||||
pointerEvents: "none",
|
||||
...b.style,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* CONTENT WRAP */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 14,
|
||||
padding: "16px 28px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<TopNav active={nav} onNavigate={setNav} />
|
||||
<HeroBar onOpenInfo={() => setDrawerOpen(true)} />
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "474px 1fr 250px",
|
||||
gap: 18,
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
}}
|
||||
>
|
||||
<ParamsPanel />
|
||||
<ResultPanel onNavigate={setNav} />
|
||||
<ObjectSummary onNavigate={setNav} />
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
{nav !== 0 && (
|
||||
<SectionOverlay
|
||||
active={nav}
|
||||
onClose={() => setNav(0)}
|
||||
onNavigate={setNav}
|
||||
/>
|
||||
)}
|
||||
<LocationDrawer open={drawerOpen} onClose={() => setDrawerOpen(false)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
551
frontend/src/components/trade-in/v2/AnalyticsView.tsx
Normal file
551
frontend/src/components/trade-in/v2/AnalyticsView.tsx
Normal file
|
|
@ -0,0 +1,551 @@
|
|||
"use client";
|
||||
|
||||
// Overlay 06 "АНАЛИТИКА ДОМА" — markup port from the МЕРА Оценка pixel design.
|
||||
// Data comes from fixtures (no API); the price-history dots navigate to the
|
||||
// "Аналогичные объявления" overlay via onNavigate?.(2).
|
||||
|
||||
import { useState } from "react";
|
||||
import { tokens } from "./tokens";
|
||||
import { analytics } from "./fixtures";
|
||||
import type { SellTimeTier } from "./types";
|
||||
|
||||
interface AnalyticsViewProps {
|
||||
onNavigate?: (i: number) => void;
|
||||
}
|
||||
|
||||
// ---- price-history geometry -----------------------------------------------
|
||||
// Parse the SVG polyline point strings into coordinates so the markers and
|
||||
// the transparent hover targets can be derived from the same source.
|
||||
|
||||
interface Pt {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
function parsePoints(s: string): Pt[] {
|
||||
return s
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.map((pair) => {
|
||||
const [x, y] = pair.split(",").map(Number);
|
||||
return { x, y };
|
||||
});
|
||||
}
|
||||
|
||||
const avitoPts = parsePoints(analytics.priceHistory.avito);
|
||||
const yandexPts = parsePoints(analytics.priceHistory.yandex);
|
||||
// Яндекс only draws dots where its series diverges from Avito (последние 3 года).
|
||||
const yandexDots = yandexPts.filter(
|
||||
(p, i) => !avitoPts[i] || p.x !== avitoPts[i].x || p.y !== avitoPts[i].y,
|
||||
);
|
||||
const hoverPts: Pt[] = [...avitoPts, ...yandexDots];
|
||||
|
||||
// X positions of the year axis labels (chart scaffolding, not in fixtures).
|
||||
const histYearX = [50, 250, 460, 670, 860];
|
||||
|
||||
// ---- sell-time tier tints --------------------------------------------------
|
||||
// One-off decorative tints per variant (design lines 553-556). The soft border
|
||||
// hex (#bfe3d2 / #e3d2ad / #e6c3c3) and the rgba background fills are documented
|
||||
// one-off design tints with no token equivalent; the label text colour reuses
|
||||
// semantic tokens (success / accent / warn / danger).
|
||||
|
||||
const tierStyles: Record<
|
||||
SellTimeTier["variant"],
|
||||
{ bg: string; border: string; label: string }
|
||||
> = {
|
||||
success: {
|
||||
bg: "rgba(27,170,107,.07)",
|
||||
border: "1px solid #bfe3d2",
|
||||
label: tokens.success,
|
||||
},
|
||||
accent: {
|
||||
bg: "rgba(46,139,255,.08)",
|
||||
border: `1.5px solid ${tokens.accent}`,
|
||||
label: tokens.accent,
|
||||
},
|
||||
gold: {
|
||||
bg: "rgba(201,154,62,.09)",
|
||||
border: "1px solid #e3d2ad",
|
||||
label: tokens.warn,
|
||||
},
|
||||
danger: {
|
||||
bg: "rgba(214,90,90,.07)",
|
||||
border: "1px solid #e6c3c3",
|
||||
label: tokens.danger,
|
||||
},
|
||||
};
|
||||
|
||||
const cardStyle = {
|
||||
background: tokens.surface.w55,
|
||||
border: `1px solid ${tokens.line2}`,
|
||||
borderRadius: 8,
|
||||
padding: "16px 18px",
|
||||
} as const;
|
||||
|
||||
export default function AnalyticsView({ onNavigate }: AnalyticsViewProps) {
|
||||
const [hoverHist, setHoverHist] = useState(false);
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 22 }}>
|
||||
{/* header */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-end",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: tokens.ink2 }}>
|
||||
Аналитика дома
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: tokens.muted2 }}>
|
||||
{analytics.header.ads} · {analytics.header.radius}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPI row */}
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(3,1fr)",
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
{analytics.kpis.map((kpi, i) => (
|
||||
<div key={i} style={cardStyle}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 9.5,
|
||||
letterSpacing: 1.5,
|
||||
color: tokens.muted2,
|
||||
}}
|
||||
>
|
||||
{kpi.label}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 30,
|
||||
fontWeight: 300,
|
||||
color: kpi.color ?? tokens.ink2,
|
||||
margin: "8px 0 5px",
|
||||
}}
|
||||
>
|
||||
{kpi.value}
|
||||
{kpi.unit ? (
|
||||
<>
|
||||
{" "}
|
||||
<span style={{ fontSize: 14, color: tokens.muted }}>
|
||||
{kpi.unit}
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: tokens.hint }}>{kpi.sub}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* срок продажи в зависимости от цены */}
|
||||
<div style={cardStyle}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: tokens.ink2 }}>
|
||||
Срок продажи в зависимости от цены
|
||||
</div>
|
||||
<div
|
||||
style={{ fontSize: 10, color: tokens.muted2, margin: "4px 0 14px" }}
|
||||
>
|
||||
{analytics.sellTimeNote}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(4,1fr)",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
{analytics.sellTime.map((tier, i) => {
|
||||
const v = tierStyles[tier.variant];
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
background: v.bg,
|
||||
border: v.border,
|
||||
borderRadius: 7,
|
||||
padding: "13px 15px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ fontSize: 9.5, color: v.label, letterSpacing: 0.5 }}
|
||||
>
|
||||
{tier.tier}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 26,
|
||||
fontWeight: 300,
|
||||
color: tokens.ink2,
|
||||
margin: "6px 0 4px",
|
||||
}}
|
||||
>
|
||||
{tier.days}
|
||||
</div>
|
||||
<div
|
||||
style={{ fontSize: 9.5, color: tokens.hint, lineHeight: 1.5 }}
|
||||
>
|
||||
{tier.range}
|
||||
<br />
|
||||
{tier.count}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* история цен */}
|
||||
<div style={{ position: "relative", ...cardStyle }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-end",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: tokens.ink2 }}>
|
||||
История цен в этом доме
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: tokens.muted2, marginTop: 4 }}>
|
||||
{analytics.priceHistory.note}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 16, fontSize: 10 }}>
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
color: tokens.muted,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{ width: 14, height: 2, background: tokens.accent }}
|
||||
/>
|
||||
Avito
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
color: tokens.muted,
|
||||
}}
|
||||
>
|
||||
<span style={{ width: 14, height: 2, background: tokens.gold }} />
|
||||
Яндекс
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
viewBox="0 0 900 220"
|
||||
style={{ width: "100%", height: 200, marginTop: 12 }}
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
<g stroke={tokens.lineSoft2} strokeWidth={1}>
|
||||
<line x1={40} y1={20} x2={900} y2={20} />
|
||||
<line x1={40} y1={65} x2={900} y2={65} />
|
||||
<line x1={40} y1={110} x2={900} y2={110} />
|
||||
<line x1={40} y1={155} x2={900} y2={155} />
|
||||
<line x1={40} y1={195} x2={900} y2={195} />
|
||||
</g>
|
||||
<g fontFamily={tokens.font.mono} fontSize={11} fill={tokens.muted2}>
|
||||
<text x={0} y={24}>
|
||||
200k
|
||||
</text>
|
||||
<text x={0} y={69}>
|
||||
150k
|
||||
</text>
|
||||
<text x={0} y={114}>
|
||||
100k
|
||||
</text>
|
||||
<text x={0} y={159}>
|
||||
50k
|
||||
</text>
|
||||
<text x={8} y={199}>
|
||||
0k
|
||||
</text>
|
||||
</g>
|
||||
<g fontFamily={tokens.font.mono} fontSize={11} fill={tokens.muted2}>
|
||||
{analytics.priceHistory.years.map((yr, i) => (
|
||||
<text key={yr} x={histYearX[i]} y={214}>
|
||||
{yr}
|
||||
</text>
|
||||
))}
|
||||
</g>
|
||||
<polyline
|
||||
points={analytics.priceHistory.avito}
|
||||
fill="none"
|
||||
stroke={tokens.accent}
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
<polyline
|
||||
points={analytics.priceHistory.yandex}
|
||||
fill="none"
|
||||
stroke={tokens.gold}
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
{/* white dot fill on an accent ring — documented one-off, no token (design l.568) */}
|
||||
<g
|
||||
fill="#fff"
|
||||
stroke={tokens.accent}
|
||||
strokeWidth={2}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<title>Архивные объявления Avito · нажмите, чтобы открыть</title>
|
||||
{avitoPts.map((p, i) => (
|
||||
<circle key={i} cx={p.x} cy={p.y} r={3.5} />
|
||||
))}
|
||||
</g>
|
||||
<g
|
||||
fill={tokens.gold}
|
||||
stroke={tokens.gold}
|
||||
strokeWidth={2}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{yandexDots.map((p, i) => (
|
||||
<circle key={i} cx={p.x} cy={p.y} r={3.5} />
|
||||
))}
|
||||
</g>
|
||||
<g
|
||||
fill="transparent"
|
||||
style={{ cursor: "pointer" }}
|
||||
onMouseEnter={() => setHoverHist(true)}
|
||||
onMouseLeave={() => setHoverHist(false)}
|
||||
onClick={() => onNavigate?.(2)}
|
||||
>
|
||||
{hoverPts.map((p, i) => (
|
||||
<circle key={i} cx={p.x} cy={p.y} r={13} />
|
||||
))}
|
||||
</g>
|
||||
</svg>
|
||||
{hoverHist ? (
|
||||
// Dark navy tooltip surface + white text — documented one-off literals
|
||||
// matching the design (l.572); no token equivalent.
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 64,
|
||||
top: 54,
|
||||
background: "rgba(23,38,58,.96)",
|
||||
color: "#fff",
|
||||
borderRadius: 7,
|
||||
padding: "9px 13px",
|
||||
fontSize: 11,
|
||||
lineHeight: 1.5,
|
||||
zIndex: 6,
|
||||
pointerEvents: "none",
|
||||
boxShadow: "0 10px 26px rgba(20,40,70,.28)",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 600 }}>
|
||||
Архивные объявления этого дома
|
||||
</div>
|
||||
<div style={{ color: tokens.barMid, fontSize: 10, marginTop: 2 }}>
|
||||
Нажмите, чтобы открыть список →
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* цена × срок продажи (детально) */}
|
||||
<div style={cardStyle}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-end",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: tokens.ink2 }}>
|
||||
Цена × срок продажи
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: tokens.muted2, marginTop: 4 }}>
|
||||
Зависимость цены от срока экспозиции ·{" "}
|
||||
{analytics.scatterDetail.note}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 14, fontSize: 10 }}>
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
color: tokens.muted,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "50%",
|
||||
background: tokens.accent,
|
||||
}}
|
||||
/>
|
||||
аналоги
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
color: tokens.muted,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "50%",
|
||||
background: tokens.scatterDeal,
|
||||
}}
|
||||
/>
|
||||
сделки
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
color: tokens.muted,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "50%",
|
||||
background: tokens.ink,
|
||||
}}
|
||||
/>
|
||||
объект
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
viewBox="0 0 900 300"
|
||||
style={{ width: "100%", height: 280, marginTop: 12 }}
|
||||
>
|
||||
<g stroke={tokens.lineSoft2} strokeWidth={1}>
|
||||
<line x1={60} y1={20} x2={880} y2={20} />
|
||||
<line x1={60} y1={77} x2={880} y2={77} />
|
||||
<line x1={60} y1={134} x2={880} y2={134} />
|
||||
<line x1={60} y1={191} x2={880} y2={191} />
|
||||
<line x1={60} y1={250} x2={880} y2={250} />
|
||||
<line x1={265} y1={20} x2={265} y2={250} />
|
||||
<line x1={470} y1={20} x2={470} y2={250} />
|
||||
<line x1={675} y1={20} x2={675} y2={250} />
|
||||
</g>
|
||||
<line
|
||||
x1={60}
|
||||
y1={20}
|
||||
x2={60}
|
||||
y2={250}
|
||||
stroke={tokens.line3}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<line
|
||||
x1={60}
|
||||
y1={250}
|
||||
x2={880}
|
||||
y2={250}
|
||||
stroke={tokens.line3}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<line
|
||||
x1={100}
|
||||
y1={226}
|
||||
x2={820}
|
||||
y2={55}
|
||||
stroke={tokens.accent}
|
||||
strokeWidth={1.5}
|
||||
strokeDasharray="5 5"
|
||||
opacity={0.45}
|
||||
/>
|
||||
<g fill={tokens.scatterDeal}>
|
||||
{analytics.scatterDetail.deals.map((p, i) => (
|
||||
<circle key={i} cx={p.x} cy={p.y} r={p.r} />
|
||||
))}
|
||||
</g>
|
||||
<g fill={tokens.accent}>
|
||||
{analytics.scatterDetail.analogs.map((p, i) => (
|
||||
<circle key={i} cx={p.x} cy={p.y} r={p.r} />
|
||||
))}
|
||||
</g>
|
||||
<circle
|
||||
cx={analytics.scatterDetail.subject.x}
|
||||
cy={analytics.scatterDetail.subject.y}
|
||||
r={analytics.scatterDetail.subject.r}
|
||||
fill={tokens.ink}
|
||||
/>
|
||||
<circle
|
||||
cx={analytics.scatterDetail.subject.x}
|
||||
cy={analytics.scatterDetail.subject.y}
|
||||
r={11}
|
||||
fill="none"
|
||||
stroke={tokens.ink}
|
||||
strokeWidth={1.2}
|
||||
opacity={0.4}
|
||||
/>
|
||||
<g
|
||||
fontFamily={tokens.font.mono}
|
||||
fontSize={11}
|
||||
fill={tokens.muted2}
|
||||
textAnchor="end"
|
||||
>
|
||||
{analytics.scatterDetail.yTicks.map((t) => (
|
||||
<text key={t.label} x={52} y={t.y}>
|
||||
{t.label}
|
||||
</text>
|
||||
))}
|
||||
</g>
|
||||
<g
|
||||
fontFamily={tokens.font.mono}
|
||||
fontSize={11}
|
||||
fill={tokens.muted2}
|
||||
textAnchor="middle"
|
||||
>
|
||||
{analytics.scatterDetail.xTicks.map((t) => (
|
||||
<text key={t.label} x={t.x} y={268}>
|
||||
{t.label}
|
||||
</text>
|
||||
))}
|
||||
</g>
|
||||
<text
|
||||
x={470}
|
||||
y={288}
|
||||
fontSize={10}
|
||||
letterSpacing={1}
|
||||
fill={tokens.muted2}
|
||||
textAnchor="middle"
|
||||
>
|
||||
СРОК ПРОДАЖИ, ДН
|
||||
</text>
|
||||
<text
|
||||
x={18}
|
||||
y={135}
|
||||
fontSize={10}
|
||||
letterSpacing={1}
|
||||
fill={tokens.muted2}
|
||||
textAnchor="middle"
|
||||
transform="rotate(-90 18 135)"
|
||||
>
|
||||
ЦЕНА ПРОДАЖИ, ₽
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
147
frontend/src/components/trade-in/v2/CacheView.tsx
Normal file
147
frontend/src/components/trade-in/v2/CacheView.tsx
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
// OVERLAY 07: ПРЕДЫДУЩИЕ ОЦЕНКИ (cache view).
|
||||
// Faithful markup port of МЕРА Оценка.dc.html lines 593-613.
|
||||
// Data from fixtures (cacheKpi + cacheRows); no API, no local state.
|
||||
|
||||
import { tokens } from "./tokens";
|
||||
import { cacheKpi, cacheRows } from "./fixtures";
|
||||
|
||||
const GRID_COLUMNS = "2.4fr 1fr 1fr 1fr";
|
||||
|
||||
export function CacheView() {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 22 }}>
|
||||
{/* 3 KPI cards */}
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(3,1fr)",
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
{cacheKpi.map((kpi) => (
|
||||
<div
|
||||
key={kpi.label}
|
||||
style={{
|
||||
background: tokens.surface.w55,
|
||||
border: `1px solid ${tokens.line2}`,
|
||||
borderRadius: 8,
|
||||
padding: "16px 18px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 9.5,
|
||||
letterSpacing: 1.5,
|
||||
color: tokens.muted2,
|
||||
}}
|
||||
>
|
||||
{kpi.label}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 30,
|
||||
fontWeight: 300,
|
||||
color: tokens.ink2,
|
||||
margin: "8px 0 5px",
|
||||
}}
|
||||
>
|
||||
{kpi.value}
|
||||
{kpi.unit && (
|
||||
<>
|
||||
{kpi.unit !== "%" && " "}
|
||||
<span style={{ fontSize: 14, color: tokens.muted }}>
|
||||
{kpi.unit}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: tokens.hint }}>{kpi.sub}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Последние оценки table */}
|
||||
<div
|
||||
style={{
|
||||
background: tokens.surface.w55,
|
||||
border: `1px solid ${tokens.line2}`,
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: "14px 18px",
|
||||
borderBottom: `1px solid ${tokens.lineSoft}`,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: tokens.ink2,
|
||||
}}
|
||||
>
|
||||
Последние оценки
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: GRID_COLUMNS,
|
||||
gap: 10,
|
||||
padding: "9px 18px",
|
||||
fontSize: 9,
|
||||
letterSpacing: 1,
|
||||
color: tokens.muted2,
|
||||
borderBottom: `1px solid ${tokens.lineSoft2}`,
|
||||
}}
|
||||
>
|
||||
<span>АДРЕС</span>
|
||||
<span>ВРЕМЯ</span>
|
||||
<span>ИСТОЧНИКОВ</span>
|
||||
<span>СТАТУС</span>
|
||||
</div>
|
||||
{cacheRows.map((r, i) => (
|
||||
<div
|
||||
key={`${r.addr}-${i}`}
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: GRID_COLUMNS,
|
||||
gap: 10,
|
||||
padding: "12px 18px",
|
||||
fontSize: 11,
|
||||
borderBottom: `1px solid ${tokens.lineSoft3}`,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<span style={{ color: tokens.body }}>{r.addr}</span>
|
||||
<span
|
||||
style={{ fontFamily: tokens.font.mono, color: tokens.muted2 }}
|
||||
>
|
||||
{r.time}
|
||||
</span>
|
||||
<span style={{ fontFamily: tokens.font.mono, color: tokens.muted }}>
|
||||
{r.src}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
fontSize: 10,
|
||||
color: r.statusColor,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 7,
|
||||
height: 7,
|
||||
borderRadius: "50%",
|
||||
background: r.statusColor,
|
||||
}}
|
||||
/>
|
||||
{r.status}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
123
frontend/src/components/trade-in/v2/Footer.tsx
Normal file
123
frontend/src/components/trade-in/v2/Footer.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
// Report footer for the /trade-in/v2 "МЕРА Оценка" design port.
|
||||
// Faithful markup port of the design footer (МЕРА Оценка.dc.html, lines 426-439):
|
||||
// report id / date / valid-until on the left, a decorative centre line, and the
|
||||
// МЕРА v2.0.6 wordmark on the right. Static markup, data from fixtures.
|
||||
|
||||
import { tokens } from "./tokens";
|
||||
import { report, version } from "./fixtures";
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: 36,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
borderTop: "1px solid rgba(150,175,200,.4)",
|
||||
marginTop: 14,
|
||||
flex: "0 0 auto",
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 10,
|
||||
letterSpacing: ".5px",
|
||||
color: tokens.muted2,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", gap: 22 }}>
|
||||
<span>
|
||||
ОТЧЁТ <span style={{ color: tokens.muted }}>{report.id}</span>
|
||||
</span>
|
||||
<span>
|
||||
ДАТА <span style={{ color: tokens.muted }}>{report.date}</span>
|
||||
</span>
|
||||
<span>
|
||||
ДЕЙСТВИТЕЛЕН ДО{" "}
|
||||
<span style={{ color: tokens.muted }}>{report.validUntil}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
maxWidth: 420,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 3,
|
||||
height: 3,
|
||||
borderRadius: "50%",
|
||||
background: tokens.muted4,
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
width: 3,
|
||||
height: 3,
|
||||
borderRadius: "50%",
|
||||
background: tokens.muted4,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 1,
|
||||
background: `linear-gradient(90deg,transparent,${tokens.lineGradient} 30%,${tokens.accent} 50%,${tokens.lineGradient} 70%,transparent)`,
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
width: 3,
|
||||
height: 3,
|
||||
borderRadius: "50%",
|
||||
background: tokens.muted4,
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
width: 3,
|
||||
height: 3,
|
||||
borderRadius: "50%",
|
||||
background: tokens.muted4,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 9 }}>
|
||||
<span
|
||||
style={{
|
||||
width: 5,
|
||||
height: 5,
|
||||
borderRadius: "50%",
|
||||
background: tokens.accent,
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: tokens.font.sans,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
letterSpacing: "4px",
|
||||
color: tokens.muted,
|
||||
}}
|
||||
>
|
||||
МЕРА
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 9,
|
||||
letterSpacing: "1px",
|
||||
color: tokens.muted4,
|
||||
}}
|
||||
>
|
||||
{version}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
484
frontend/src/components/trade-in/v2/HeroBar.tsx
Normal file
484
frontend/src/components/trade-in/v2/HeroBar.tsx
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
"use client";
|
||||
|
||||
import { tokens } from "./tokens";
|
||||
import { object, report } from "./fixtures";
|
||||
|
||||
interface HeroBarProps {
|
||||
onOpenInfo: () => void;
|
||||
}
|
||||
|
||||
export default function HeroBar({ onOpenInfo }: HeroBarProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 30,
|
||||
padding: "14px 2px 12px",
|
||||
flex: "0 0 auto",
|
||||
}}
|
||||
>
|
||||
<style>{`
|
||||
.hero-pdf-btn:hover { border-color: ${tokens.accent}; background: rgba(46,139,255,.07); }
|
||||
.hero-pdf-btn:active { transform: translateY(1px); }
|
||||
.hero-calc-btn:hover { border-color: ${tokens.accent}; color: ${tokens.accent}; }
|
||||
.hero-coef-row:hover { background: rgba(46,139,255,.09); }
|
||||
@keyframes hero-scanv { 0% { transform: translateY(-100%); } 100% { transform: translateY(900%); } }
|
||||
`}</style>
|
||||
|
||||
{/* LEFT: meta + buttons */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
gap: 22,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", gap: 26, alignItems: "flex-start" }}>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 9,
|
||||
letterSpacing: "2px",
|
||||
color: tokens.muted2,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
ОТЧЁТ
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 14,
|
||||
color: tokens.ink,
|
||||
}}
|
||||
>
|
||||
{report.id}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
borderLeft: `1px solid ${tokens.lineSoft}`,
|
||||
paddingLeft: 26,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 9,
|
||||
letterSpacing: "2px",
|
||||
color: tokens.muted2,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
ДАТА
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 14,
|
||||
color: tokens.ink,
|
||||
}}
|
||||
>
|
||||
{report.date}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
borderLeft: `1px solid ${tokens.lineSoft}`,
|
||||
paddingLeft: 26,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 9,
|
||||
letterSpacing: "2px",
|
||||
color: tokens.muted2,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
ДЕЙСТВИТЕЛЕН ДО
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 14,
|
||||
color: tokens.ink,
|
||||
}}
|
||||
>
|
||||
{report.validUntil}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: 12, width: "100%", maxWidth: 440 }}>
|
||||
<button
|
||||
type="button"
|
||||
className="hero-pdf-btn"
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 50,
|
||||
background: tokens.surface.w55,
|
||||
border: `1px solid ${tokens.line}`,
|
||||
borderRadius: 7,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 11,
|
||||
padding: "0 16px",
|
||||
cursor: "pointer",
|
||||
fontFamily: tokens.font.sans,
|
||||
color: tokens.ink,
|
||||
transition: "all .15s",
|
||||
}}
|
||||
>
|
||||
<svg width="18" height="20" viewBox="0 0 18 20" fill="none">
|
||||
<path d="M2 1h9l5 5v13H2z" stroke="#2e8bff" strokeWidth="1.2" />
|
||||
<path d="M11 1v5h5" stroke="#2e8bff" strokeWidth="1.2" />
|
||||
<line x1="5" y1="11" x2="13" y2="11" stroke="#6f8195" />
|
||||
<line x1="5" y1="14" x2="13" y2="14" stroke="#6f8195" />
|
||||
</svg>
|
||||
<span
|
||||
style={{ fontSize: 11, fontWeight: 600, letterSpacing: "1px" }}
|
||||
>
|
||||
СКАЧАТЬ PDF-ОТЧЁТ
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="hero-calc-btn"
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 50,
|
||||
background: tokens.surface.w40,
|
||||
border: `1px solid ${tokens.line}`,
|
||||
borderRadius: 7,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
fontFamily: tokens.font.sans,
|
||||
color: tokens.muted,
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
letterSpacing: "1.5px",
|
||||
transition: "all .15s",
|
||||
}}
|
||||
>
|
||||
КАК РАССЧИТАНО
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT: photo + overlays */}
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
width: 560,
|
||||
height: 152,
|
||||
flex: "0 0 auto",
|
||||
border: `1px solid ${tokens.line3}`,
|
||||
borderRadius: 6,
|
||||
overflow: "hidden",
|
||||
background: tokens.photoBg,
|
||||
boxShadow: "0 6px 26px rgba(40,80,130,.10)",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/trade-in-v2/building.png"
|
||||
alt=""
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "contain",
|
||||
objectPosition: "right center",
|
||||
filter: "saturate(.9) brightness(1.04)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
background:
|
||||
"linear-gradient(90deg,rgba(238,244,250,.96) 0%,rgba(238,244,250,.5) 22%,transparent 40%)",
|
||||
}}
|
||||
/>
|
||||
<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",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* address card */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 16,
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
width: 182,
|
||||
background: tokens.surface.w72,
|
||||
backdropFilter: "blur(5px)",
|
||||
border: `1px solid ${tokens.line}`,
|
||||
borderRadius: 5,
|
||||
padding: "11px 13px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 9,
|
||||
letterSpacing: "2.5px",
|
||||
color: tokens.muted2,
|
||||
marginBottom: 3,
|
||||
}}
|
||||
>
|
||||
АДРЕС
|
||||
</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, lineHeight: 1.3 }}>
|
||||
{object.address}
|
||||
<br />
|
||||
<span
|
||||
style={{ fontSize: 11, fontWeight: 400, color: tokens.muted }}
|
||||
>
|
||||
{object.city}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{ height: 1, background: tokens.lineSoft, margin: "8px 0" }}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 9,
|
||||
color: tokens.muted,
|
||||
lineHeight: 1.55,
|
||||
}}
|
||||
>
|
||||
{object.streetView}
|
||||
</div>
|
||||
<div
|
||||
style={{ height: 1, background: tokens.lineSoft, margin: "8px 0" }}
|
||||
/>
|
||||
<div
|
||||
className="hero-coef-row"
|
||||
onClick={onOpenInfo}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
cursor: "pointer",
|
||||
borderRadius: 4,
|
||||
margin: "-3px -5px",
|
||||
padding: "3px 5px",
|
||||
transition: "background .15s",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "8.5px",
|
||||
letterSpacing: "1.5px",
|
||||
color: tokens.muted2,
|
||||
}}
|
||||
>
|
||||
КОЭФ. ЛОКАЦИИ
|
||||
</span>
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 15,
|
||||
fontWeight: 500,
|
||||
color: tokens.accent,
|
||||
}}
|
||||
>
|
||||
{object.locationCoef}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 14,
|
||||
height: 14,
|
||||
border: `1px solid ${tokens.accent}`,
|
||||
borderRadius: "50%",
|
||||
fontSize: "8.5px",
|
||||
color: tokens.accent,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
?
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<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",
|
||||
}}
|
||||
>
|
||||
{object.compass}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* distance scale */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
bottom: 16,
|
||||
width: 170,
|
||||
fontFamily: tokens.font.mono,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-end",
|
||||
height: 8,
|
||||
borderBottom: "1px solid rgba(46,139,255,.55)",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 1,
|
||||
height: 8,
|
||||
background: "rgba(46,139,255,.55)",
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
width: 1,
|
||||
height: 5,
|
||||
background: "rgba(46,139,255,.45)",
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
width: 1,
|
||||
height: 5,
|
||||
background: "rgba(46,139,255,.45)",
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
width: 1,
|
||||
height: 5,
|
||||
background: "rgba(46,139,255,.45)",
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
width: 1,
|
||||
height: 8,
|
||||
background: "rgba(46,139,255,.55)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
fontSize: "7.5px",
|
||||
color: tokens.muted,
|
||||
marginTop: 3,
|
||||
}}
|
||||
>
|
||||
<span>0</span>
|
||||
<span>25</span>
|
||||
<span>50</span>
|
||||
<span>75</span>
|
||||
<span>100м</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* corner brackets */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 8,
|
||||
top: 8,
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderLeft: `1.5px solid ${tokens.accent}`,
|
||||
borderTop: `1.5px solid ${tokens.accent}`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 8,
|
||||
top: 8,
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRight: `1.5px solid ${tokens.accent}`,
|
||||
borderTop: `1.5px solid ${tokens.accent}`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 8,
|
||||
bottom: 8,
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderLeft: `1.5px solid ${tokens.accent}`,
|
||||
borderBottom: `1.5px solid ${tokens.accent}`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRight: `1.5px solid ${tokens.accent}`,
|
||||
borderBottom: `1.5px solid ${tokens.accent}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
359
frontend/src/components/trade-in/v2/HistoryView.tsx
Normal file
359
frontend/src/components/trade-in/v2/HistoryView.tsx
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
// Overlay 04 "ПРОДАЖИ В ДОМЕ" body for the /trade-in/v2 "МЕРА Оценка" port.
|
||||
// Faithful markup port of the design's showHistory block (МЕРА Оценка.dc.html,
|
||||
// lines 457-489): "История продаж в этом доме" card (house sales) +
|
||||
// "ДКП-сделки на улице" card (3 KPI + dealine table + footer count).
|
||||
// Static markup — data comes from fixtures, no fetch / no local state.
|
||||
|
||||
import { tokens } from "./tokens";
|
||||
import { history, dkpRows } from "./fixtures";
|
||||
|
||||
// Shared grid templates (kept verbatim from the design column tracks).
|
||||
const houseGrid = "2fr 1.4fr 1fr 1fr";
|
||||
const dkpGrid = "2.2fr 1fr 1.3fr 1fr .9fr 2fr";
|
||||
|
||||
export default function HistoryView() {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "22px" }}>
|
||||
{/* ── История продаж в этом доме ───────────────────────────── */}
|
||||
<div
|
||||
style={{
|
||||
background: tokens.surface.w55,
|
||||
border: `1px solid ${tokens.line2}`,
|
||||
borderRadius: "8px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "14px 18px",
|
||||
borderBottom: `1px solid ${tokens.lineSoft}`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
letterSpacing: "1px",
|
||||
color: tokens.ink2,
|
||||
}}
|
||||
>
|
||||
История продаж в этом доме
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "10px",
|
||||
letterSpacing: "1px",
|
||||
color: tokens.muted2,
|
||||
}}
|
||||
>
|
||||
{history.houseSales.length} ЛОТА
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: houseGrid,
|
||||
gap: "12px",
|
||||
padding: "9px 18px",
|
||||
fontSize: "9px",
|
||||
letterSpacing: "1.2px",
|
||||
color: tokens.muted2,
|
||||
borderBottom: `1px solid ${tokens.lineSoft2}`,
|
||||
}}
|
||||
>
|
||||
<span>ЛОТ</span>
|
||||
<span>ЦЕНА НАЧ. → ПОСЛ.</span>
|
||||
<span>ДАТА</span>
|
||||
<span>ЭКСПОЗИЦИЯ</span>
|
||||
</div>
|
||||
|
||||
{history.houseSales.map((row, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: houseGrid,
|
||||
gap: "12px",
|
||||
padding: "13px 18px",
|
||||
fontSize: "12px",
|
||||
borderBottom:
|
||||
i < history.houseSales.length - 1
|
||||
? `1px solid ${tokens.lineSoft4}`
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<span style={{ color: tokens.body }}>{row.lot}</span>
|
||||
<span style={{ fontFamily: tokens.font.mono, color: tokens.muted }}>
|
||||
{row.priceFrom} →{" "}
|
||||
<b style={{ color: tokens.ink2 }}>{row.priceTo}</b>
|
||||
</span>
|
||||
<span style={{ fontFamily: tokens.font.mono, color: tokens.muted }}>
|
||||
{row.date}
|
||||
</span>
|
||||
<span style={{ fontFamily: tokens.font.mono, color: tokens.muted }}>
|
||||
{row.exposure}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ── ДКП-сделки на улице ──────────────────────────────────── */}
|
||||
<div
|
||||
style={{
|
||||
background: tokens.surface.w55,
|
||||
border: `1px solid ${tokens.line2}`,
|
||||
borderRadius: "8px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: "14px 18px",
|
||||
borderBottom: `1px solid ${tokens.lineSoft}`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "8.5px",
|
||||
letterSpacing: "2px",
|
||||
color: tokens.muted2,
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
ПОХОЖИЕ КВАРТИРЫ · ПО ВАШЕЙ УЛИЦЕ
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
letterSpacing: ".5px",
|
||||
color: tokens.ink2,
|
||||
}}
|
||||
>
|
||||
{history.dkpHeader.title}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
textAlign: "right",
|
||||
fontSize: "9.5px",
|
||||
color: tokens.muted2,
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
Период:{" "}
|
||||
<b style={{ color: tokens.muted }}>{history.dkpHeader.period}</b>
|
||||
<br />
|
||||
Источник: Росреестр + объявления
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "10px",
|
||||
color: tokens.hint,
|
||||
marginTop: "8px",
|
||||
}}
|
||||
>
|
||||
{history.dkpHeader.note}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPI band */}
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(3,1fr)",
|
||||
borderBottom: `1px solid ${tokens.lineSoft}`,
|
||||
background: tokens.surfaceTint,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: "12px 18px",
|
||||
borderRight: `1px solid ${tokens.lineSoft2}`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "9px",
|
||||
letterSpacing: "1.5px",
|
||||
color: tokens.muted2,
|
||||
}}
|
||||
>
|
||||
СДЕЛОК ЗА 24 МЕС
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: "22px",
|
||||
fontWeight: 300,
|
||||
color: tokens.ink2,
|
||||
marginTop: "4px",
|
||||
}}
|
||||
>
|
||||
{history.dkpKpi.count}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
padding: "12px 18px",
|
||||
borderRight: `1px solid ${tokens.lineSoft2}`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "9px",
|
||||
letterSpacing: "1.5px",
|
||||
color: tokens.muted2,
|
||||
}}
|
||||
>
|
||||
МЕДИАНА
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: "22px",
|
||||
fontWeight: 300,
|
||||
color: tokens.ink2,
|
||||
marginTop: "4px",
|
||||
}}
|
||||
>
|
||||
{history.dkpKpi.median}{" "}
|
||||
<span style={{ fontSize: "11px", color: tokens.muted }}>
|
||||
₽/м²
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: "12px 18px" }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "9px",
|
||||
letterSpacing: "1.5px",
|
||||
color: tokens.muted2,
|
||||
}}
|
||||
>
|
||||
ДИАПАЗОН
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: "22px",
|
||||
fontWeight: 300,
|
||||
color: tokens.accent,
|
||||
marginTop: "4px",
|
||||
}}
|
||||
>
|
||||
{history.dkpKpi.range}{" "}
|
||||
<span style={{ fontSize: "11px", color: tokens.muted }}>
|
||||
млн ₽
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table header */}
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: dkpGrid,
|
||||
gap: "10px",
|
||||
padding: "9px 18px",
|
||||
fontSize: "9px",
|
||||
letterSpacing: "1px",
|
||||
color: tokens.muted2,
|
||||
borderBottom: `1px solid ${tokens.lineSoft2}`,
|
||||
}}
|
||||
>
|
||||
<span>АДРЕС</span>
|
||||
<span>ПЛОЩАДЬ</span>
|
||||
<span>ЦЕНА СДЕЛКИ</span>
|
||||
<span>₽/М²</span>
|
||||
<span>ДАТА</span>
|
||||
<span>ОБЪЯВЛЕНИЕ ДО СДЕЛКИ</span>
|
||||
</div>
|
||||
|
||||
{dkpRows.map((r, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: dkpGrid,
|
||||
gap: "10px",
|
||||
padding: "11px 18px",
|
||||
fontSize: "11px",
|
||||
borderBottom: `1px solid ${tokens.lineSoft3}`,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<span style={{ color: tokens.body }}>
|
||||
Екатеринбург, Академика Ландау
|
||||
</span>
|
||||
<span style={{ fontFamily: tokens.font.mono, color: tokens.muted }}>
|
||||
{r.area}
|
||||
</span>
|
||||
<span style={{ fontFamily: tokens.font.mono, color: tokens.ink2 }}>
|
||||
{r.price}
|
||||
</span>
|
||||
<span style={{ fontFamily: tokens.font.mono, color: tokens.muted }}>
|
||||
{r.ppm}
|
||||
</span>
|
||||
<span
|
||||
style={{ fontFamily: tokens.font.mono, color: tokens.muted2 }}
|
||||
>
|
||||
01.01.26
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "7px",
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: "10px",
|
||||
color: tokens.muted,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "8px",
|
||||
letterSpacing: ".5px",
|
||||
background: tokens.badgeTint,
|
||||
border: `1px solid ${tokens.line2}`,
|
||||
borderRadius: "3px",
|
||||
padding: "1px 5px",
|
||||
color: tokens.accent,
|
||||
}}
|
||||
>
|
||||
Яндекс
|
||||
</span>
|
||||
{r.ask}
|
||||
<span style={{ color: r.deltaColor }}>{r.delta}</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div
|
||||
style={{
|
||||
padding: "11px 18px",
|
||||
fontSize: "10px",
|
||||
color: tokens.hint,
|
||||
background: tokens.surfaceTint,
|
||||
}}
|
||||
>
|
||||
Показано {dkpRows.length} из {history.dkpKpi.count} сделок
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
330
frontend/src/components/trade-in/v2/LocationDrawer.tsx
Normal file
330
frontend/src/components/trade-in/v2/LocationDrawer.tsx
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
"use client";
|
||||
|
||||
// "КОЭФФИЦИЕНТ ЛОКАЦИИ" right-side drawer for the /trade-in/v2 "МЕРА Оценка"
|
||||
// design port. Faithful markup port of the design drawer (МЕРА Оценка.dc.html,
|
||||
// lines 618-660): scrim + 452px right slider that slides in via translateX, a
|
||||
// big location coefficient, intro, base×coef formula, the factors that raise /
|
||||
// lower price, and a geo-source footer. Static markup, data from fixtures.
|
||||
// Open/close is driven entirely by props (no internal state, no fetch).
|
||||
|
||||
import { tokens } from "./tokens";
|
||||
import type { LocationFactor } from "./types";
|
||||
import { locationFactors } from "./fixtures";
|
||||
|
||||
interface LocationDrawerProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function FactorRow({
|
||||
factor,
|
||||
variant,
|
||||
}: {
|
||||
factor: LocationFactor;
|
||||
variant: "pos" | "neg";
|
||||
}) {
|
||||
const isPos = variant === "pos";
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
fontSize: 12,
|
||||
color: tokens.body,
|
||||
}}
|
||||
>
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 9 }}>
|
||||
<span
|
||||
style={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderRadius: 4,
|
||||
background: isPos
|
||||
? "rgba(27,170,107,.12)"
|
||||
: "rgba(111,129,149,.14)",
|
||||
color: isPos ? tokens.success : tokens.muted,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: isPos ? 11 : 12,
|
||||
}}
|
||||
>
|
||||
{isPos ? "+" : "−"}
|
||||
</span>
|
||||
{factor.label}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 11,
|
||||
color: isPos ? tokens.success : tokens.muted,
|
||||
}}
|
||||
>
|
||||
{factor.delta}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LocationDrawer({ open, onClose }: LocationDrawerProps) {
|
||||
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>
|
||||
|
||||
{/* big coefficient */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
gap: 14,
|
||||
marginTop: 24,
|
||||
paddingBottom: 20,
|
||||
borderBottom: `1px solid ${tokens.lineSoft}`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 52,
|
||||
fontWeight: 300,
|
||||
color: tokens.accent,
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{locationFactors.coef}
|
||||
</span>
|
||||
<div style={{ fontSize: 11, color: tokens.muted, lineHeight: 1.5 }}>
|
||||
множитель к базовой
|
||||
<br />
|
||||
цене по городу
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* intro */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12.5,
|
||||
lineHeight: 1.7,
|
||||
color: tokens.body2,
|
||||
marginTop: 20,
|
||||
}}
|
||||
>
|
||||
Показывает, как адрес корректирует стоимость относительно медианы по
|
||||
Екатеринбургу. <b style={{ color: tokens.ink2 }}>1.00</b> — средний
|
||||
уровень. <b style={{ color: tokens.accent }}>0.87</b> означает, что
|
||||
локация снижает цену примерно на{" "}
|
||||
<b style={{ color: tokens.ink2 }}>13%</b> из-за баланса факторов ниже.
|
||||
</div>
|
||||
|
||||
{/* formula */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
marginTop: 18,
|
||||
background: tokens.surface.w55,
|
||||
border: `1px solid ${tokens.lineSoft}`,
|
||||
borderRadius: 7,
|
||||
padding: "12px 14px",
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 12,
|
||||
color: tokens.ink2,
|
||||
}}
|
||||
>
|
||||
<span style={{ color: tokens.muted }}>
|
||||
{locationFactors.formula.base}
|
||||
</span>
|
||||
<span style={{ color: tokens.muted2 }}>×</span>
|
||||
<span style={{ color: tokens.accent }}>
|
||||
{locationFactors.formula.coef}
|
||||
</span>
|
||||
<span style={{ color: tokens.muted2 }}>=</span>
|
||||
<span style={{ fontWeight: 500 }}>
|
||||
{locationFactors.formula.result}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* positives */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 9,
|
||||
letterSpacing: "2px",
|
||||
color: tokens.muted2,
|
||||
marginTop: 26,
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
ЧТО ПОВЫШАЕТ ЦЕНУ
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 9 }}>
|
||||
{locationFactors.positives.map((f, i) => (
|
||||
<FactorRow key={i} factor={f} variant="pos" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* negatives */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 9,
|
||||
letterSpacing: "2px",
|
||||
color: tokens.muted2,
|
||||
marginTop: 24,
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
ЧТО СНИЖАЕТ ЦЕНУ
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 9 }}>
|
||||
{locationFactors.negatives.map((f, i) => (
|
||||
<FactorRow key={i} factor={f} variant="neg" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* footer source */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: 24,
|
||||
paddingTop: 16,
|
||||
borderTop: `1px solid ${tokens.lineSoft}`,
|
||||
fontSize: 10.5,
|
||||
lineHeight: 1.6,
|
||||
color: tokens.hint,
|
||||
}}
|
||||
>
|
||||
{locationFactors.footer}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
302
frontend/src/components/trade-in/v2/ObjectSummary.tsx
Normal file
302
frontend/src/components/trade-in/v2/ObjectSummary.tsx
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
// 03 СВОДКА ОБЪЕКТА — right-rail summary card for the /trade-in/v2 "МЕРА Оценка"
|
||||
// design port. Faithful markup port of МЕРА Оценка.dc.html lines 397-422:
|
||||
// address + "В РАСЧЁТЕ" badge, four clickable per-section totals, and a
|
||||
// "КАЧЕСТВО ДАННЫХ" block with a donut. Data from fixtures; clicks fire
|
||||
// onNavigate(row.nav) only — no API, no fetching.
|
||||
|
||||
"use client";
|
||||
|
||||
import { tokens } from "./tokens";
|
||||
import { object, summary } from "./fixtures";
|
||||
|
||||
interface ObjectSummaryProps {
|
||||
onNavigate: (i: number) => void;
|
||||
}
|
||||
|
||||
export function ObjectSummary({ onNavigate }: ObjectSummaryProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
background: tokens.surface.w50,
|
||||
backdropFilter: "blur(6px)",
|
||||
border: `1px solid ${tokens.line2}`,
|
||||
borderRadius: 8,
|
||||
padding: "15px 16px",
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
color: tokens.ink,
|
||||
fontFamily: tokens.font.sans,
|
||||
}}
|
||||
>
|
||||
<style>{`
|
||||
.os-row { transition: background .12s; }
|
||||
.os-row:hover { background: rgba(46,139,255,.07); }
|
||||
`}</style>
|
||||
|
||||
{/* HUD corner bracket */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: -1,
|
||||
top: -1,
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRight: `1.5px solid ${tokens.bracket}`,
|
||||
borderTop: `1.5px solid ${tokens.bracket}`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Header: 03 СВОДКА ОБЪЕКТА + В РАСЧЁТЕ badge */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 5,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "baseline", gap: 9 }}>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 14,
|
||||
color: tokens.accent,
|
||||
}}
|
||||
>
|
||||
03
|
||||
</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 600, letterSpacing: 1.5 }}>
|
||||
СВОДКА ОБЪЕКТА
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
fontSize: 8,
|
||||
letterSpacing: 0.8,
|
||||
color: tokens.success,
|
||||
border: `1px solid ${tokens.successBorder}`,
|
||||
borderRadius: 20,
|
||||
padding: "2px 7px",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 5,
|
||||
height: 5,
|
||||
borderRadius: "50%",
|
||||
background: tokens.success,
|
||||
}}
|
||||
/>
|
||||
В РАСЧЁТЕ
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: 9,
|
||||
letterSpacing: 1.2,
|
||||
color: tokens.muted3,
|
||||
marginBottom: 13,
|
||||
}}
|
||||
>
|
||||
ИТОГИ ПО РАЗДЕЛАМ
|
||||
</div>
|
||||
|
||||
{/* Address */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: 9,
|
||||
marginBottom: 14,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="15"
|
||||
height="18"
|
||||
viewBox="0 0 15 18"
|
||||
fill="none"
|
||||
style={{ marginTop: 1 }}
|
||||
>
|
||||
<path
|
||||
d="M7.5 17C7.5 17 14 11 14 6.5A6.5 6.5 0 1 0 1 6.5C1 11 7.5 17 7.5 17Z"
|
||||
stroke={tokens.accent}
|
||||
strokeWidth="1.2"
|
||||
/>
|
||||
<circle cx="7.5" cy="6.5" r="2.3" fill={tokens.accent} />
|
||||
</svg>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 600 }}>{object.address}</div>
|
||||
<div style={{ fontSize: 11, color: tokens.muted, marginTop: 2 }}>
|
||||
{object.city}
|
||||
</div>
|
||||
</div>
|
||||
<svg width="13" height="13" viewBox="0 0 13 13" fill="none">
|
||||
<path
|
||||
d="M5 1h7v7M12 1L6 7M9 8v4H1V4h4"
|
||||
stroke={tokens.muted2}
|
||||
strokeWidth="1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Per-section totals (clickable) */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
|
||||
{summary.rows.map((row, i) => (
|
||||
<div
|
||||
key={row.label}
|
||||
className="os-row"
|
||||
onClick={() => onNavigate(row.nav)}
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "9px 6px",
|
||||
borderBottom:
|
||||
i < summary.rows.length - 1
|
||||
? `1px dotted ${tokens.lineGradient}`
|
||||
: undefined,
|
||||
cursor: "pointer",
|
||||
borderRadius: 5,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 10.5,
|
||||
color: tokens.body,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
whiteSpace: "nowrap",
|
||||
paddingRight: 12,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 5,
|
||||
height: 5,
|
||||
borderRadius: "50%",
|
||||
background:
|
||||
row.dot === "accent" ? tokens.accent : tokens.scatterDeal,
|
||||
}}
|
||||
/>
|
||||
{row.label}
|
||||
</span>
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 11,
|
||||
color: tokens.ink2,
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{row.value}
|
||||
</span>
|
||||
<span style={{ color: tokens.accent, fontSize: 11 }}>↗</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* КАЧЕСТВО ДАННЫХ */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: 14,
|
||||
background: tokens.infoSoftBg,
|
||||
border: `1px solid ${tokens.infoSoftBorder}`,
|
||||
borderRadius: 6,
|
||||
padding: 12,
|
||||
display: "flex",
|
||||
gap: 12,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<svg width="50" height="50" viewBox="0 0 50 50" fill="none">
|
||||
<circle
|
||||
cx="25"
|
||||
cy="25"
|
||||
r="20"
|
||||
stroke={tokens.track}
|
||||
strokeWidth="3"
|
||||
/>
|
||||
<circle
|
||||
cx="25"
|
||||
cy="25"
|
||||
r="20"
|
||||
stroke={tokens.accent}
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray="126"
|
||||
strokeDashoffset="72"
|
||||
transform="rotate(-90 25 25)"
|
||||
/>
|
||||
<circle cx="25" cy="25" r="11" stroke="#cdddec" strokeWidth="1" />
|
||||
<circle cx="25" cy="25" r="1.6" fill={tokens.accent} />
|
||||
</svg>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 9.5,
|
||||
letterSpacing: 1.5,
|
||||
color: tokens.muted2,
|
||||
marginBottom: 7,
|
||||
}}
|
||||
>
|
||||
КАЧЕСТВО ДАННЫХ
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
fontSize: 10,
|
||||
color: tokens.muted,
|
||||
lineHeight: 1.7,
|
||||
}}
|
||||
>
|
||||
<span>Источников</span>
|
||||
<span style={{ fontFamily: tokens.font.mono, color: tokens.ink }}>
|
||||
{summary.quality.sources}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
fontSize: 10,
|
||||
color: tokens.muted,
|
||||
lineHeight: 1.7,
|
||||
}}
|
||||
>
|
||||
<span>Достоверность</span>
|
||||
<span style={{ color: tokens.ink }}>
|
||||
{summary.quality.confidence}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
fontSize: 10,
|
||||
color: tokens.muted,
|
||||
lineHeight: 1.7,
|
||||
}}
|
||||
>
|
||||
<span>CV</span>
|
||||
<span
|
||||
style={{ fontFamily: tokens.font.mono, color: tokens.accent }}
|
||||
>
|
||||
{summary.quality.cv}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
958
frontend/src/components/trade-in/v2/ParamsPanel.tsx
Normal file
958
frontend/src/components/trade-in/v2/ParamsPanel.tsx
Normal file
|
|
@ -0,0 +1,958 @@
|
|||
"use client";
|
||||
|
||||
// 01 ПАРАМЕТРЫ КВАРТИРЫ — left card of the /trade-in/v2 "МЕРА Оценка" port.
|
||||
// Faithful markup port (МЕРА Оценка.dc.html lines 146-240). Markup-first: all
|
||||
// values come from fixtures (no API). Dropdowns / balcony toggle drive local UI
|
||||
// state only. Hover/active + @keyframes live in a pp-prefixed local <style>.
|
||||
|
||||
import { useState, type CSSProperties } from "react";
|
||||
import { tokens } from "./tokens";
|
||||
import { object, dropdownOptions } from "./fixtures";
|
||||
|
||||
type DdKey = "rooms" | "houseType" | "repair" | "radius" | "crm" | null;
|
||||
|
||||
interface DdProps {
|
||||
open: boolean;
|
||||
onToggle: () => void;
|
||||
value: string;
|
||||
options: string[];
|
||||
onSelect: (v: string) => void;
|
||||
mono: boolean;
|
||||
triggerFontSize: number;
|
||||
optionFontSize: number;
|
||||
variant?: "solid" | "dashed";
|
||||
openUp?: boolean;
|
||||
triggerHeight?: number;
|
||||
triggerBg?: string;
|
||||
triggerColor?: string;
|
||||
caretColor?: string;
|
||||
}
|
||||
|
||||
function Dd({
|
||||
open,
|
||||
onToggle,
|
||||
value,
|
||||
options,
|
||||
onSelect,
|
||||
mono,
|
||||
triggerFontSize,
|
||||
optionFontSize,
|
||||
variant = "solid",
|
||||
openUp = false,
|
||||
triggerHeight = 33,
|
||||
triggerBg = tokens.surface.w62,
|
||||
triggerColor = tokens.ink,
|
||||
caretColor = tokens.muted2,
|
||||
}: DdProps) {
|
||||
const panelStyle: CSSProperties = {
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
background: tokens.surface.w98,
|
||||
border: `1px solid ${tokens.line}`,
|
||||
borderRadius: 6,
|
||||
zIndex: 30,
|
||||
padding: 3,
|
||||
...(openUp
|
||||
? {
|
||||
bottom: "calc(100% + 4px)",
|
||||
boxShadow: "0 -10px 26px rgba(40,80,130,.18)",
|
||||
}
|
||||
: {
|
||||
top: "calc(100% + 4px)",
|
||||
boxShadow: "0 10px 26px rgba(40,80,130,.18)",
|
||||
}),
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
<div
|
||||
className={
|
||||
variant === "dashed" ? "pp-dd-trigger-dashed" : "pp-dd-trigger"
|
||||
}
|
||||
onClick={onToggle}
|
||||
style={{
|
||||
height: triggerHeight,
|
||||
background: triggerBg,
|
||||
borderRadius: 6,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "0 13px",
|
||||
fontFamily: mono ? tokens.font.mono : tokens.font.sans,
|
||||
fontSize: triggerFontSize,
|
||||
color: triggerColor,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
<span
|
||||
style={{
|
||||
color: caretColor,
|
||||
fontSize: 9,
|
||||
fontFamily: tokens.font.sans,
|
||||
}}
|
||||
>
|
||||
▼
|
||||
</span>
|
||||
</div>
|
||||
{open && (
|
||||
<div style={panelStyle}>
|
||||
{options.map((o) => (
|
||||
<div
|
||||
key={o}
|
||||
className="pp-dd-opt"
|
||||
onClick={() => onSelect(o)}
|
||||
style={{
|
||||
padding: "7px 10px",
|
||||
fontSize: optionFontSize,
|
||||
fontFamily: mono ? tokens.font.mono : tokens.font.sans,
|
||||
borderRadius: 4,
|
||||
cursor: "pointer",
|
||||
color: o === value ? tokens.accent : tokens.ink,
|
||||
fontWeight: o === value ? 600 : 400,
|
||||
}}
|
||||
>
|
||||
{o}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = `
|
||||
@keyframes pp-pulsedot{0%,100%{opacity:.45}50%{opacity:1}}
|
||||
.pp-dot{animation:pp-pulsedot 2.6s ease-in-out infinite}
|
||||
.pp-dd-trigger{border:1px solid ${tokens.line};transition:border-color .15s}
|
||||
.pp-dd-trigger:hover{border-color:${tokens.accent}}
|
||||
.pp-dd-trigger-dashed{border:1px dashed ${tokens.line};transition:border-color .15s}
|
||||
.pp-dd-trigger-dashed:hover{border-color:${tokens.accent}}
|
||||
.pp-dd-opt{transition:background .15s}
|
||||
.pp-dd-opt:hover{background:rgba(46,139,255,.1)}
|
||||
.pp-eval-btn{background:linear-gradient(90deg,rgba(255,255,255,.7),rgba(233,242,252,.7));box-shadow:0 0 0 0 rgba(46,139,255,0);transition:all .18s}
|
||||
.pp-eval-btn:hover{background:linear-gradient(90deg,rgba(46,139,255,.12),rgba(46,139,255,.18));box-shadow:0 6px 22px rgba(46,139,255,.28)}
|
||||
.pp-eval-btn:active{transform:translateY(1px)}
|
||||
`;
|
||||
|
||||
const hintLabel: CSSProperties = {
|
||||
fontSize: 9.5,
|
||||
letterSpacing: 1.5,
|
||||
color: tokens.muted2,
|
||||
marginBottom: 3,
|
||||
};
|
||||
|
||||
const optHint: CSSProperties = {
|
||||
fontSize: 9,
|
||||
color: tokens.muted4,
|
||||
};
|
||||
|
||||
const fieldRow: CSSProperties = {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 1fr",
|
||||
gap: 12,
|
||||
};
|
||||
|
||||
const textField: CSSProperties = {
|
||||
height: 33,
|
||||
background: tokens.surface.w62,
|
||||
border: `1px solid ${tokens.line}`,
|
||||
borderRadius: 6,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "0 13px",
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 14,
|
||||
};
|
||||
|
||||
export default function ParamsPanel() {
|
||||
const [openDd, setOpenDd] = useState<DdKey>(null);
|
||||
const [rooms, setRooms] = useState(object.rooms);
|
||||
const [houseType, setHouseType] = useState(object.houseType);
|
||||
const [repair, setRepair] = useState(object.repair);
|
||||
const [radius, setRadius] = useState("500 м");
|
||||
const [crm, setCrm] = useState(dropdownOptions.crm[0]);
|
||||
const [balcony, setBalcony] = useState(object.balcony);
|
||||
|
||||
const toggle = (k: Exclude<DdKey, null>) =>
|
||||
setOpenDd((cur) => (cur === k ? null : k));
|
||||
const pick = (setter: (v: string) => void) => (v: string) => {
|
||||
setter(v);
|
||||
setOpenDd(null);
|
||||
};
|
||||
|
||||
const balOn: CSSProperties = {
|
||||
border: `1px solid ${tokens.accent}`,
|
||||
color: tokens.accent,
|
||||
fontWeight: 600,
|
||||
background: "rgba(46,139,255,.1)",
|
||||
};
|
||||
const balOff: CSSProperties = {
|
||||
border: `1px solid ${tokens.line}`,
|
||||
color: tokens.muted,
|
||||
fontWeight: 400,
|
||||
background: "transparent",
|
||||
};
|
||||
const balBase: CSSProperties = {
|
||||
flex: 1,
|
||||
borderRadius: 6,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: 11,
|
||||
letterSpacing: 1,
|
||||
cursor: "pointer",
|
||||
transition: "all .15s",
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
background: tokens.surface.w50,
|
||||
backdropFilter: "blur(6px)",
|
||||
border: `1px solid ${tokens.line2}`,
|
||||
borderRadius: 8,
|
||||
padding: "16px 18px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<style>{styles}</style>
|
||||
|
||||
{/* HUD corner brackets */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: -1,
|
||||
top: -1,
|
||||
width: 13,
|
||||
height: 13,
|
||||
borderLeft: `1.5px solid ${tokens.bracket}`,
|
||||
borderTop: `1.5px solid ${tokens.bracket}`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: -1,
|
||||
bottom: -1,
|
||||
width: 13,
|
||||
height: 13,
|
||||
borderRight: `1.5px solid ${tokens.bracket}`,
|
||||
borderBottom: `1.5px solid ${tokens.bracket}`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* header */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 13,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "baseline", gap: 10 }}>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 15,
|
||||
color: tokens.accent,
|
||||
}}
|
||||
>
|
||||
01
|
||||
</span>
|
||||
<span style={{ fontSize: 13, fontWeight: 600, letterSpacing: 2 }}>
|
||||
ПАРАМЕТРЫ КВАРТИРЫ
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 10,
|
||||
letterSpacing: 1,
|
||||
color: tokens.muted2,
|
||||
}}
|
||||
>
|
||||
ШАГ 1/1
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* MAP */}
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
height: 140,
|
||||
border: `1px solid ${tokens.line}`,
|
||||
borderRadius: 6,
|
||||
overflow: "hidden",
|
||||
background: tokens.mapBg,
|
||||
flex: "0 0 auto",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 440 210"
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<rect width="440" height="210" fill="#e6edf4" />
|
||||
<g stroke="#dbe5ef" strokeWidth={1}>
|
||||
<path d="M0 38H440M0 78H440M0 118H440M0 158H440M0 198H440" />
|
||||
<path d="M40 0V210M110 0V210M180 0V210M250 0V210M320 0V210M390 0V210" />
|
||||
</g>
|
||||
<g stroke="#f5f8fc" strokeWidth={8} strokeLinecap="round">
|
||||
<path d="M-10 92 H450" />
|
||||
<path d="M150 -10 V220" />
|
||||
<path d="M-10 30 L260 0" />
|
||||
</g>
|
||||
<g stroke="#eef3f9" strokeWidth={5}>
|
||||
<path d="M300 -10 L470 150" />
|
||||
<path d="M-10 170 H450" />
|
||||
</g>
|
||||
<path d="M150 92 L470 200" stroke="#cfe0f3" strokeWidth={6} />
|
||||
<g fill="none" stroke="#2e8bff" strokeDasharray="3 4">
|
||||
<circle cx="208" cy="100" r="44" opacity={0.55} />
|
||||
<circle cx="208" cy="100" r="80" opacity={0.4} />
|
||||
<circle cx="208" cy="100" r="112" opacity={0.28} />
|
||||
</g>
|
||||
<circle cx="208" cy="100" r="80" fill="#2e8bff" opacity={0.04} />
|
||||
</svg>
|
||||
|
||||
{/* center pin */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "47%",
|
||||
top: "48%",
|
||||
transform: "translate(-50%,-100%)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
background: tokens.surface.w85,
|
||||
border: `1px solid ${tokens.accent}`,
|
||||
borderRadius: 4,
|
||||
padding: "4px 8px",
|
||||
whiteSpace: "nowrap",
|
||||
boxShadow: "0 3px 10px rgba(46,139,255,.25)",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 7,
|
||||
height: 7,
|
||||
borderRadius: "50%",
|
||||
background: tokens.accent,
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: 11, fontWeight: 600 }}>
|
||||
{object.address}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeft: "5px solid transparent",
|
||||
borderRight: "5px solid transparent",
|
||||
borderTop: `7px solid ${tokens.accent}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* analog price pins */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "5%",
|
||||
top: "13%",
|
||||
background: tokens.surface.w70,
|
||||
border: `1px solid ${tokens.line}`,
|
||||
borderRadius: 4,
|
||||
padding: "3px 7px",
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 9,
|
||||
lineHeight: 1.4,
|
||||
color: tokens.ink,
|
||||
}}
|
||||
>
|
||||
12,7 млн ₽<br />
|
||||
<span style={{ color: tokens.muted }}>54 м²</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: "32%",
|
||||
top: "8%",
|
||||
background: tokens.surface.w70,
|
||||
border: `1px solid ${tokens.line}`,
|
||||
borderRadius: 4,
|
||||
padding: "3px 7px",
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 9,
|
||||
lineHeight: 1.4,
|
||||
color: tokens.ink,
|
||||
}}
|
||||
>
|
||||
7,6 млн ₽<br />
|
||||
<span style={{ color: tokens.muted }}>44 м²</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: "3%",
|
||||
top: "36%",
|
||||
background: tokens.surface.w70,
|
||||
border: `1px solid ${tokens.line}`,
|
||||
borderRadius: 4,
|
||||
padding: "3px 7px",
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 9,
|
||||
lineHeight: 1.4,
|
||||
color: tokens.ink,
|
||||
}}
|
||||
>
|
||||
22,4 млн ₽<br />
|
||||
<span style={{ color: tokens.muted }}>55 м²</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "13%",
|
||||
bottom: "9%",
|
||||
background: tokens.surface.w70,
|
||||
border: `1px solid ${tokens.line}`,
|
||||
borderRadius: 4,
|
||||
padding: "3px 7px",
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 9,
|
||||
lineHeight: 1.4,
|
||||
color: tokens.ink,
|
||||
}}
|
||||
>
|
||||
15,9 млн ₽<br />
|
||||
<span style={{ color: tokens.muted }}>55 м²</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: "14%",
|
||||
bottom: "11%",
|
||||
background: tokens.surface.w70,
|
||||
border: `1px solid ${tokens.line}`,
|
||||
borderRadius: 4,
|
||||
padding: "3px 7px",
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 9,
|
||||
lineHeight: 1.4,
|
||||
color: tokens.ink,
|
||||
}}
|
||||
>
|
||||
14,9 млн ₽<br />
|
||||
<span style={{ color: tokens.muted }}>56 м²</span>
|
||||
</div>
|
||||
|
||||
{/* analog dots */}
|
||||
<span
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "18%",
|
||||
top: "30%",
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "50%",
|
||||
background: tokens.accent,
|
||||
border: `1.5px solid ${tokens.dotStroke}`,
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: "34%",
|
||||
top: "28%",
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "50%",
|
||||
background: tokens.dealDot,
|
||||
border: `1.5px solid ${tokens.dotStroke}`,
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "30%",
|
||||
bottom: "24%",
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "50%",
|
||||
background: tokens.accent,
|
||||
border: `1.5px solid ${tokens.dotStroke}`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* map controls */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 9,
|
||||
bottom: 9,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 5,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
background: tokens.surface.w80,
|
||||
border: `1px solid ${tokens.line}`,
|
||||
borderRadius: 4,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
transition: "all .15s",
|
||||
fontSize: 15,
|
||||
color: tokens.muted,
|
||||
}}
|
||||
>
|
||||
+
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
background: tokens.surface.w80,
|
||||
border: `1px solid ${tokens.line}`,
|
||||
borderRadius: 4,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
transition: "all .15s",
|
||||
fontSize: 15,
|
||||
color: tokens.muted,
|
||||
}}
|
||||
>
|
||||
−
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
background: tokens.surface.w80,
|
||||
border: `1px solid ${tokens.line}`,
|
||||
borderRadius: 4,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
transition: "all .15s",
|
||||
color: tokens.accent,
|
||||
}}
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 13 13" fill="none">
|
||||
<circle cx="6.5" cy="6.5" r="4" stroke="#2e8bff" />
|
||||
<line x1="6.5" y1="0" x2="6.5" y2="2.5" stroke="#2e8bff" />
|
||||
<line x1="6.5" y1="10.5" x2="6.5" y2="13" stroke="#2e8bff" />
|
||||
<line x1="0" y1="6.5" x2="2.5" y2="6.5" stroke="#2e8bff" />
|
||||
<line x1="10.5" y1="6.5" x2="13" y2="6.5" stroke="#2e8bff" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* map corner bracket */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 8,
|
||||
top: 8,
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderLeft: `1.5px solid ${tokens.accent}`,
|
||||
borderTop: `1.5px solid ${tokens.accent}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* radius row */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 14,
|
||||
marginTop: 13,
|
||||
flex: "0 0 auto",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 10, letterSpacing: 1.5, color: tokens.muted }}>
|
||||
РАДИУС АНАЛИЗА
|
||||
</span>
|
||||
<div style={{ position: "relative" }}>
|
||||
<div
|
||||
onClick={() => toggle("radius")}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
background: tokens.surface.w60,
|
||||
border: `1px solid ${tokens.line}`,
|
||||
borderRadius: 5,
|
||||
padding: "6px 10px",
|
||||
fontSize: 12,
|
||||
fontFamily: tokens.font.mono,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<span>{radius}</span>
|
||||
<span style={{ color: tokens.muted2, fontSize: 9 }}>▼</span>
|
||||
</div>
|
||||
{openDd === "radius" && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: "calc(100% + 4px)",
|
||||
background: tokens.surface.w98,
|
||||
border: `1px solid ${tokens.line}`,
|
||||
borderRadius: 6,
|
||||
boxShadow: "0 10px 26px rgba(40,80,130,.18)",
|
||||
zIndex: 30,
|
||||
padding: 3,
|
||||
minWidth: 78,
|
||||
}}
|
||||
>
|
||||
{dropdownOptions.radius.map((o) => (
|
||||
<div
|
||||
key={o}
|
||||
className="pp-dd-opt"
|
||||
onClick={() => pick(setRadius)(o)}
|
||||
style={{
|
||||
padding: "7px 10px",
|
||||
fontSize: 13,
|
||||
fontFamily: tokens.font.mono,
|
||||
borderRadius: 4,
|
||||
cursor: "pointer",
|
||||
color: o === radius ? tokens.accent : tokens.ink,
|
||||
fontWeight: o === radius ? 600 : 400,
|
||||
}}
|
||||
>
|
||||
{o}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 16,
|
||||
fontSize: 10.5,
|
||||
color: tokens.muted,
|
||||
marginLeft: "auto",
|
||||
}}
|
||||
>
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<span
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "50%",
|
||||
background: tokens.accent,
|
||||
}}
|
||||
/>
|
||||
Объявления
|
||||
</span>
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<span
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "50%",
|
||||
background: tokens.dealDot,
|
||||
}}
|
||||
/>
|
||||
Сделки
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* INPUTS */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: 10,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 5,
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div style={hintLabel}>АДРЕС (YANDEX / NOMINATIM)</div>
|
||||
<div style={{ position: "relative" }}>
|
||||
<div
|
||||
style={{
|
||||
height: 33,
|
||||
background: tokens.surface.w62,
|
||||
border: `1px solid ${tokens.line}`,
|
||||
borderRadius: 6,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "0 38px 0 13px",
|
||||
fontSize: 13,
|
||||
color: tokens.ink,
|
||||
}}
|
||||
>
|
||||
{object.address} · Куйбышева, 48…
|
||||
</div>
|
||||
<svg
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 12,
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
}}
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
>
|
||||
<circle cx="7.5" cy="7.5" r="4.5" stroke="#2e8bff" />
|
||||
<line x1="7.5" y1="0" x2="7.5" y2="3" stroke="#2e8bff" />
|
||||
<line x1="7.5" y1="12" x2="7.5" y2="15" stroke="#2e8bff" />
|
||||
<line x1="0" y1="7.5" x2="3" y2="7.5" stroke="#2e8bff" />
|
||||
<line x1="12" y1="7.5" x2="15" y2="7.5" stroke="#2e8bff" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={fieldRow}>
|
||||
<div>
|
||||
<div style={hintLabel}>ПЛОЩАДЬ, М²</div>
|
||||
<div style={textField}>{object.area}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={hintLabel}>КОМНАТ</div>
|
||||
<Dd
|
||||
open={openDd === "rooms"}
|
||||
onToggle={() => toggle("rooms")}
|
||||
value={rooms}
|
||||
options={dropdownOptions.rooms}
|
||||
onSelect={pick(setRooms)}
|
||||
mono
|
||||
triggerFontSize={14}
|
||||
optionFontSize={13}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={fieldRow}>
|
||||
<div>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<span style={hintLabel}>ЭТАЖ</span>
|
||||
<span style={optHint}>если знаешь</span>
|
||||
</div>
|
||||
<div style={textField}>{object.floor}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<span style={hintLabel}>ВСЕГО ЭТАЖЕЙ</span>
|
||||
<span style={optHint}>если знаешь</span>
|
||||
</div>
|
||||
<div style={textField}>{object.totalFloors}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: 10,
|
||||
color: tokens.hint,
|
||||
lineHeight: 1.4,
|
||||
marginTop: -3,
|
||||
}}
|
||||
>
|
||||
1-й и последний этаж снижают цену на 5–10% — заполни если знаешь
|
||||
</div>
|
||||
|
||||
<div style={fieldRow}>
|
||||
<div>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<span style={hintLabel}>ГОД ПОСТРОЙКИ</span>
|
||||
<span style={optHint}>опц.</span>
|
||||
</div>
|
||||
<div style={textField}>{object.year}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<span style={hintLabel}>ТИП ДОМА</span>
|
||||
<span style={optHint}>опц.</span>
|
||||
</div>
|
||||
<Dd
|
||||
open={openDd === "houseType"}
|
||||
onToggle={() => toggle("houseType")}
|
||||
value={houseType}
|
||||
options={dropdownOptions.houseType}
|
||||
onSelect={pick(setHouseType)}
|
||||
mono={false}
|
||||
triggerFontSize={13}
|
||||
optionFontSize={12}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={fieldRow}>
|
||||
<div>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<span style={hintLabel}>СОСТОЯНИЕ РЕМОНТА</span>
|
||||
<span style={optHint}>опц.</span>
|
||||
</div>
|
||||
<Dd
|
||||
open={openDd === "repair"}
|
||||
onToggle={() => toggle("repair")}
|
||||
value={repair}
|
||||
options={dropdownOptions.repair}
|
||||
onSelect={pick(setRepair)}
|
||||
mono={false}
|
||||
triggerFontSize={13}
|
||||
optionFontSize={12}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div style={hintLabel}>БАЛКОН / ЛОДЖИЯ</div>
|
||||
<div style={{ height: 40, display: "flex", gap: 6 }}>
|
||||
<div
|
||||
onClick={() => setBalcony(false)}
|
||||
style={{ ...balBase, ...(balcony ? balOff : balOn) }}
|
||||
>
|
||||
НЕТ
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setBalcony(true)}
|
||||
style={{ ...balBase, ...(balcony ? balOn : balOff) }}
|
||||
>
|
||||
ДА
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<span style={hintLabel}>CRM — ДЛЯ МЕНЕДЖЕРА</span>
|
||||
<span style={optHint}>опц.</span>
|
||||
</div>
|
||||
<Dd
|
||||
open={openDd === "crm"}
|
||||
onToggle={() => toggle("crm")}
|
||||
value={crm}
|
||||
options={dropdownOptions.crm}
|
||||
onSelect={pick(setCrm)}
|
||||
mono={false}
|
||||
triggerFontSize={12}
|
||||
optionFontSize={12}
|
||||
variant="dashed"
|
||||
openUp
|
||||
triggerHeight={34}
|
||||
triggerBg={tokens.surface.w50}
|
||||
triggerColor={tokens.hint}
|
||||
caretColor={tokens.muted4}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* button */}
|
||||
<button
|
||||
type="button"
|
||||
className="pp-eval-btn"
|
||||
style={{
|
||||
marginTop: 9,
|
||||
height: 46,
|
||||
width: "100%",
|
||||
border: `1px solid ${tokens.accent}`,
|
||||
borderRadius: 7,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
position: "relative",
|
||||
cursor: "pointer",
|
||||
fontFamily: tokens.font.sans,
|
||||
color: tokens.ink,
|
||||
flex: "0 0 auto",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 14, fontWeight: 600, letterSpacing: 3 }}>
|
||||
ОЦЕНИТЬ КВАРТИРУ
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 16,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 26,
|
||||
height: 26,
|
||||
border: `1px solid ${tokens.accent}`,
|
||||
borderRadius: "50%",
|
||||
color: tokens.accent,
|
||||
}}
|
||||
>
|
||||
→
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* cache status */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginTop: 11,
|
||||
flex: "0 0 auto",
|
||||
fontSize: 9.5,
|
||||
letterSpacing: 1.5,
|
||||
color: tokens.muted2,
|
||||
}}
|
||||
>
|
||||
<span>КЭШ ПО АДРЕСУ — 24 Ч</span>
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
color: tokens.success,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="pp-dot"
|
||||
style={{
|
||||
width: 7,
|
||||
height: 7,
|
||||
borderRadius: "50%",
|
||||
background: tokens.success,
|
||||
boxShadow: `0 0 6px ${tokens.success}`,
|
||||
}}
|
||||
/>
|
||||
ГОТОВ
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
880
frontend/src/components/trade-in/v2/ResultPanel.tsx
Normal file
880
frontend/src/components/trade-in/v2/ResultPanel.tsx
Normal file
|
|
@ -0,0 +1,880 @@
|
|||
"use client";
|
||||
|
||||
import { Fragment } from "react";
|
||||
import { tokens } from "./tokens";
|
||||
import {
|
||||
ranges,
|
||||
resultCards,
|
||||
resultMeta,
|
||||
scatterMini,
|
||||
sources,
|
||||
} from "./fixtures";
|
||||
|
||||
const {
|
||||
accent,
|
||||
onAccent,
|
||||
accentGlow,
|
||||
ink,
|
||||
muted,
|
||||
muted2,
|
||||
muted3,
|
||||
line,
|
||||
line2,
|
||||
line3,
|
||||
bracket,
|
||||
barLo,
|
||||
barMid,
|
||||
track,
|
||||
success,
|
||||
hint,
|
||||
scatterDeal,
|
||||
disabledBorder,
|
||||
disabledValue,
|
||||
disabledLabel,
|
||||
surface,
|
||||
font,
|
||||
} = tokens;
|
||||
|
||||
interface ResultPanelProps {
|
||||
onNavigate: (i: number) => void;
|
||||
}
|
||||
|
||||
// Mini-histogram column tint: peak column = accent, its neighbours = mid, rest = lo.
|
||||
function barColor(i: number, bars: number[]): string {
|
||||
const peak = bars.indexOf(Math.max(...bars));
|
||||
if (i === peak) return accent;
|
||||
if (i === peak - 1 || i === peak + 1) return barMid;
|
||||
return barLo;
|
||||
}
|
||||
|
||||
// Render a string[] with <br/> between lines (faithful to the design line breaks).
|
||||
function lines(parts: string[]) {
|
||||
return parts.map((part, i) => (
|
||||
<Fragment key={i}>
|
||||
{i > 0 && <br />}
|
||||
{part}
|
||||
</Fragment>
|
||||
));
|
||||
}
|
||||
|
||||
// Split a source name on its parenthetical so the design's forced
|
||||
// "РОСРЕЕСТР / (ВНУТР.)" two-line break (line-height 1.3) is reproduced; plain
|
||||
// names without " (" stay on a single line.
|
||||
function sourceName(name: string) {
|
||||
const idx = name.indexOf(" (");
|
||||
if (idx === -1) return name;
|
||||
return (
|
||||
<>
|
||||
{name.slice(0, idx)}
|
||||
<br />
|
||||
{name.slice(idx + 1)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ResultPanel({ onNavigate }: ResultPanelProps) {
|
||||
return (
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", gap: 14, minWidth: 0 }}
|
||||
>
|
||||
<style>{`
|
||||
.rp-more { transition: opacity 120ms ease; }
|
||||
.rp-more:hover { opacity: .6; }
|
||||
`}</style>
|
||||
|
||||
{/* header */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "baseline", gap: 10 }}>
|
||||
<span style={{ fontFamily: font.mono, fontSize: 15, color: accent }}>
|
||||
02
|
||||
</span>
|
||||
<span style={{ fontSize: 13, fontWeight: 600, letterSpacing: 2 }}>
|
||||
РЕЗУЛЬТАТ ОЦЕНКИ
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 22,
|
||||
fontSize: 10,
|
||||
letterSpacing: 1,
|
||||
color: muted,
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
ИСТОЧНИКОВ:{" "}
|
||||
<b style={{ color: ink, fontFamily: font.mono }}>
|
||||
{resultMeta.sources}
|
||||
</b>
|
||||
</span>
|
||||
<span>
|
||||
ДОСТОВЕРНОСТЬ: <b style={{ color: ink }}>{resultMeta.confidence}</b>
|
||||
</span>
|
||||
<span>
|
||||
CV{" "}
|
||||
<b style={{ color: accent, fontFamily: font.mono }}>
|
||||
{resultMeta.cv}
|
||||
</b>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* result cards */}
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 1fr 1fr",
|
||||
gap: 14,
|
||||
flex: "0 0 auto",
|
||||
}}
|
||||
>
|
||||
{resultCards.map((card, ci) => (
|
||||
<div
|
||||
key={ci}
|
||||
style={{
|
||||
position: "relative",
|
||||
background: surface.w55,
|
||||
backdropFilter: "blur(6px)",
|
||||
border: `1px solid ${line2}`,
|
||||
borderRadius: 8,
|
||||
padding: "15px 16px 13px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: -1,
|
||||
top: -1,
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderLeft: `1.5px solid ${bracket}`,
|
||||
borderTop: `1.5px solid ${bracket}`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 9.5,
|
||||
letterSpacing: 1.5,
|
||||
color: muted2,
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
{lines(card.title)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
marginTop: 14,
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: font.mono,
|
||||
fontSize: 38,
|
||||
fontWeight: 300,
|
||||
letterSpacing: -1,
|
||||
}}
|
||||
>
|
||||
{card.value}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: muted,
|
||||
letterSpacing: 1,
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{card.unit}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: font.mono,
|
||||
fontSize: 11,
|
||||
color: muted,
|
||||
marginTop: 10,
|
||||
}}
|
||||
>
|
||||
{card.range}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: font.mono,
|
||||
fontSize: 10,
|
||||
color: muted2,
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
{card.ppm}
|
||||
</div>
|
||||
|
||||
{card.bars ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-end",
|
||||
justifyContent: "space-between",
|
||||
gap: 12,
|
||||
marginTop: 12,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-end",
|
||||
gap: 2,
|
||||
height: 20,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{card.bars.map((h, bi) => (
|
||||
<span
|
||||
key={bi}
|
||||
style={{
|
||||
flex: 1,
|
||||
height: `${h}%`,
|
||||
background: barColor(bi, card.bars as number[]),
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className="rp-more"
|
||||
onClick={() => onNavigate(card.nav)}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
cursor: "pointer",
|
||||
fontSize: 9,
|
||||
letterSpacing: 0.3,
|
||||
color: accent,
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
Подробнее <span>→</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className="rp-more"
|
||||
onClick={() => onNavigate(card.nav)}
|
||||
style={{
|
||||
marginTop: 16,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
gap: 4,
|
||||
cursor: "pointer",
|
||||
fontSize: 9,
|
||||
letterSpacing: 0.3,
|
||||
color: accent,
|
||||
}}
|
||||
>
|
||||
Подробнее <span>→</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 14,
|
||||
top: 13,
|
||||
width: 58,
|
||||
height: 58,
|
||||
}}
|
||||
>
|
||||
<svg width="58" height="58" viewBox="0 0 58 58">
|
||||
<circle
|
||||
cx="29"
|
||||
cy="29"
|
||||
r="25"
|
||||
fill="none"
|
||||
stroke={track}
|
||||
strokeWidth="3"
|
||||
/>
|
||||
<circle
|
||||
cx="29"
|
||||
cy="29"
|
||||
r="25"
|
||||
fill="none"
|
||||
stroke={accent}
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray="157"
|
||||
strokeDashoffset="140"
|
||||
transform="rotate(-90 29 29)"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: font.mono,
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
color: accent,
|
||||
}}
|
||||
>
|
||||
{card.delta}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 6.5,
|
||||
letterSpacing: 1,
|
||||
color: muted2,
|
||||
marginTop: 1,
|
||||
}}
|
||||
>
|
||||
{card.deltaLabel}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ranges + radar */}
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
background: surface.w50,
|
||||
backdropFilter: "blur(6px)",
|
||||
border: `1px solid ${line2}`,
|
||||
borderRadius: 8,
|
||||
padding: "18px 20px",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 168px 1fr",
|
||||
gap: 24,
|
||||
alignItems: "center",
|
||||
flex: "0 0 auto",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: -1,
|
||||
bottom: -1,
|
||||
width: 13,
|
||||
height: 13,
|
||||
borderRight: `1.5px solid ${bracket}`,
|
||||
borderBottom: `1.5px solid ${bracket}`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* left range — ads */}
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 9,
|
||||
letterSpacing: 1.3,
|
||||
color: muted2,
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
{lines(ranges.ads.label)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: font.mono,
|
||||
fontSize: 11,
|
||||
color: muted,
|
||||
margin: "12px 0 16px",
|
||||
}}
|
||||
>
|
||||
медиана · <b style={{ color: ink }}>{ranges.ads.median}</b>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
height: 3,
|
||||
background: track,
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: ranges.ads.marker.fillLeft,
|
||||
right: ranges.ads.marker.fillRight,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
background: `linear-gradient(90deg,${barMid},${accent})`,
|
||||
borderRadius: 2,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: ranges.ads.marker.dotLeft,
|
||||
top: "50%",
|
||||
transform: "translate(-50%,-50%)",
|
||||
width: 13,
|
||||
height: 13,
|
||||
borderRadius: "50%",
|
||||
background: onAccent,
|
||||
border: `3px solid ${accent}`,
|
||||
boxShadow: `0 0 8px ${accentGlow}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
fontFamily: font.mono,
|
||||
fontSize: 10,
|
||||
color: muted2,
|
||||
marginTop: 10,
|
||||
}}
|
||||
>
|
||||
<span>{ranges.ads.lo}</span>
|
||||
<span style={{ color: accent }}>{ranges.ads.median}</span>
|
||||
<span>{ranges.ads.hi}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* radar — price × sell-time scatter */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 8, letterSpacing: 1.5, color: muted2 }}>
|
||||
ЦЕНА × СРОК ПРОДАЖИ
|
||||
</div>
|
||||
<svg width="168" height="140" viewBox="0 0 168 140" fill="none">
|
||||
<g stroke="#e3ecf4" strokeWidth="1">
|
||||
<line x1="34" y1="38" x2="158" y2="38" />
|
||||
<line x1="34" y1="74" x2="158" y2="74" />
|
||||
<line x1="96" y1="10" x2="96" y2="110" />
|
||||
</g>
|
||||
<line
|
||||
x1="34"
|
||||
y1="10"
|
||||
x2="34"
|
||||
y2="110"
|
||||
stroke={line3}
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<line
|
||||
x1="34"
|
||||
y1="110"
|
||||
x2="158"
|
||||
y2="110"
|
||||
stroke={line3}
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<line
|
||||
x1="40"
|
||||
y1="100"
|
||||
x2="150"
|
||||
y2="30"
|
||||
stroke={accent}
|
||||
strokeWidth="1"
|
||||
strokeDasharray="3 4"
|
||||
opacity="0.45"
|
||||
/>
|
||||
<g fill={scatterDeal}>
|
||||
{scatterMini.deals.map((p, i) => (
|
||||
<circle key={i} cx={p.x} cy={p.y} r={p.r} />
|
||||
))}
|
||||
</g>
|
||||
<g fill={accent}>
|
||||
{scatterMini.analogs.map((p, i) => (
|
||||
<circle key={i} cx={p.x} cy={p.y} r={p.r} />
|
||||
))}
|
||||
</g>
|
||||
<circle
|
||||
cx={scatterMini.subject.x}
|
||||
cy={scatterMini.subject.y}
|
||||
r={scatterMini.subject.r}
|
||||
fill={ink}
|
||||
/>
|
||||
<circle
|
||||
cx={scatterMini.subject.x}
|
||||
cy={scatterMini.subject.y}
|
||||
r="6.5"
|
||||
fill="none"
|
||||
stroke={ink}
|
||||
strokeWidth="1"
|
||||
opacity="0.4"
|
||||
/>
|
||||
<g
|
||||
fontFamily={font.mono}
|
||||
fontSize="6.5"
|
||||
fill={muted2}
|
||||
textAnchor="end"
|
||||
>
|
||||
{scatterMini.yTicks.map((t, i) => (
|
||||
<text key={i} x={30} y={t.y}>
|
||||
{t.label}
|
||||
</text>
|
||||
))}
|
||||
</g>
|
||||
<g
|
||||
fontFamily={font.mono}
|
||||
fontSize="6.5"
|
||||
fill={muted2}
|
||||
textAnchor="middle"
|
||||
>
|
||||
{scatterMini.xTicks.map((t, i) => (
|
||||
<text key={i} x={t.x} y={120}>
|
||||
{t.label}
|
||||
</text>
|
||||
))}
|
||||
</g>
|
||||
<text
|
||||
x="96"
|
||||
y="134"
|
||||
fontSize="6.5"
|
||||
letterSpacing="1"
|
||||
fill={muted2}
|
||||
textAnchor="middle"
|
||||
>
|
||||
СРОК ПРОДАЖИ, ДН
|
||||
</text>
|
||||
<text
|
||||
x="11"
|
||||
y="60"
|
||||
fontSize="6.5"
|
||||
letterSpacing="1"
|
||||
fill={muted2}
|
||||
textAnchor="middle"
|
||||
transform="rotate(-90 11 60)"
|
||||
>
|
||||
ЦЕНА ПРОДАЖИ
|
||||
</text>
|
||||
</svg>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 12,
|
||||
fontSize: 7.5,
|
||||
letterSpacing: 0.5,
|
||||
color: muted2,
|
||||
}}
|
||||
>
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 4 }}>
|
||||
<span
|
||||
style={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: "50%",
|
||||
background: accent,
|
||||
}}
|
||||
/>
|
||||
аналоги
|
||||
</span>
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 4 }}>
|
||||
<span
|
||||
style={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: "50%",
|
||||
background: scatterDeal,
|
||||
}}
|
||||
/>
|
||||
сделки
|
||||
</span>
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 4 }}>
|
||||
<span
|
||||
style={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: "50%",
|
||||
background: ink,
|
||||
}}
|
||||
/>
|
||||
объект
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="rp-more"
|
||||
onClick={() => onNavigate(3)}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
cursor: "pointer",
|
||||
fontSize: 9,
|
||||
letterSpacing: 0.3,
|
||||
color: accent,
|
||||
marginTop: 2,
|
||||
}}
|
||||
>
|
||||
Подробнее <span>→</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* right range — deals */}
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 9,
|
||||
letterSpacing: 1.3,
|
||||
color: muted2,
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
{lines(ranges.deals.label)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: font.mono,
|
||||
fontSize: 11,
|
||||
color: muted,
|
||||
margin: "12px 0 16px",
|
||||
}}
|
||||
>
|
||||
медиана · <b style={{ color: ink }}>{ranges.deals.median}</b>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
height: 3,
|
||||
background: track,
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: ranges.deals.marker.fillLeft,
|
||||
right: ranges.deals.marker.fillRight,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
background: `linear-gradient(90deg,${barMid},${accent})`,
|
||||
borderRadius: 2,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: ranges.deals.marker.dotLeft,
|
||||
top: "50%",
|
||||
transform: "translate(-50%,-50%)",
|
||||
width: 13,
|
||||
height: 13,
|
||||
borderRadius: "50%",
|
||||
background: onAccent,
|
||||
border: `3px solid ${accent}`,
|
||||
boxShadow: `0 0 8px ${accentGlow}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
fontFamily: font.mono,
|
||||
fontSize: 10,
|
||||
color: muted2,
|
||||
marginTop: 10,
|
||||
}}
|
||||
>
|
||||
<span>{ranges.deals.lo}</span>
|
||||
<span style={{ color: accent }}>{ranges.deals.median}</span>
|
||||
<span>{ranges.deals.hi}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* sources */}
|
||||
<div
|
||||
style={{
|
||||
background: surface.w50,
|
||||
backdropFilter: "blur(6px)",
|
||||
border: `1px solid ${line2}`,
|
||||
borderRadius: 8,
|
||||
padding: "14px 18px 13px",
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 11,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
letterSpacing: 2,
|
||||
color: ink,
|
||||
}}
|
||||
>
|
||||
ИСТОЧНИКИ ДАННЫХ
|
||||
</span>
|
||||
<span
|
||||
className="rp-more"
|
||||
onClick={() => onNavigate(2)}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
cursor: "pointer",
|
||||
fontSize: 9.5,
|
||||
letterSpacing: 0.3,
|
||||
color: accent,
|
||||
}}
|
||||
>
|
||||
Подробнее <span>→</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(7,1fr)",
|
||||
gap: 9,
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
alignItems: "stretch",
|
||||
alignContent: "stretch",
|
||||
}}
|
||||
>
|
||||
{sources.map((s, si) =>
|
||||
s.active ? (
|
||||
<div
|
||||
key={si}
|
||||
style={{
|
||||
border: `1px solid ${line}`,
|
||||
borderRadius: 6,
|
||||
padding: "10px 9px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
background: surface.w50,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 9,
|
||||
letterSpacing: 0.5,
|
||||
color: ink,
|
||||
fontWeight: 600,
|
||||
...(s.name.includes(" (") ? { lineHeight: 1.3 } : {}),
|
||||
}}
|
||||
>
|
||||
{sourceName(s.name)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: font.mono,
|
||||
fontSize: 26,
|
||||
fontWeight: 300,
|
||||
marginTop: "auto",
|
||||
}}
|
||||
>
|
||||
{s.count}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 5,
|
||||
fontSize: 9,
|
||||
color: muted,
|
||||
marginTop: 3,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: "50%",
|
||||
background: success,
|
||||
}}
|
||||
/>
|
||||
{s.label}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
key={si}
|
||||
style={{
|
||||
border: `1px solid ${disabledBorder}`,
|
||||
borderRadius: 6,
|
||||
padding: "10px 9px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
background: surface.w25,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 9,
|
||||
letterSpacing: 0.5,
|
||||
color: muted3,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{s.name}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: font.mono,
|
||||
fontSize: 22,
|
||||
fontWeight: 300,
|
||||
marginTop: "auto",
|
||||
color: disabledValue,
|
||||
}}
|
||||
>
|
||||
{s.count}
|
||||
</div>
|
||||
<div
|
||||
style={{ fontSize: 9, color: disabledLabel, marginTop: 3 }}
|
||||
>
|
||||
{s.label}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginTop: 11,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 10, color: hint }}>
|
||||
При недоступности источника частичный результат не блокируется
|
||||
</span>
|
||||
<span style={{ fontSize: 9.5, letterSpacing: 1.5, color: muted2 }}>
|
||||
ПОСТРОЕНО ПО 6 АНАЛОГАМ И 10 СДЕЛКАМ ЗА 6 МЕСЯЦЕВ
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
150
frontend/src/components/trade-in/v2/SectionOverlay.tsx
Normal file
150
frontend/src/components/trade-in/v2/SectionOverlay.tsx
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
"use client";
|
||||
|
||||
// Overlay wrapper for the nav-driven section panels of the /trade-in/v2
|
||||
// "МЕРА Оценка" design. Faithful port of the design overlay container
|
||||
// (МЕРА Оценка.dc.html lines 443-453 + 615-616): a glass/blur absolute layer
|
||||
// with HUD corner brackets, a header (section number + title + "← К ОЦЕНКЕ"
|
||||
// button) and a scrollable body that swaps the active view.
|
||||
|
||||
import { tokens } from "./tokens";
|
||||
import { overlayNums, overlayTitles } from "./fixtures";
|
||||
import HistoryView from "./HistoryView";
|
||||
import SourcesView from "./SourcesView";
|
||||
import AnalyticsView from "./AnalyticsView";
|
||||
import { CacheView } from "./CacheView";
|
||||
|
||||
interface SectionOverlayProps {
|
||||
active: number;
|
||||
onClose: () => void;
|
||||
onNavigate: (i: number) => void;
|
||||
}
|
||||
|
||||
export default function SectionOverlay({
|
||||
active,
|
||||
onClose,
|
||||
onNavigate,
|
||||
}: SectionOverlayProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 28,
|
||||
right: 28,
|
||||
top: 80,
|
||||
bottom: 30,
|
||||
zIndex: 30,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
background:
|
||||
"linear-gradient(180deg,rgba(244,248,252,.985),rgba(235,242,250,.985))",
|
||||
backdropFilter: "blur(12px)",
|
||||
border: `1px solid ${tokens.line3}`,
|
||||
borderRadius: 8,
|
||||
boxShadow: "0 22px 60px rgba(40,80,130,.18)",
|
||||
overflow: "hidden",
|
||||
fontFamily: tokens.font.sans,
|
||||
}}
|
||||
>
|
||||
<style>{`
|
||||
.tiv2-so-back { transition: all .15s; }
|
||||
.tiv2-so-back:hover { border-color: ${tokens.accent}; color: ${tokens.accent}; }
|
||||
`}</style>
|
||||
|
||||
{/* HUD corner brackets */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 12,
|
||||
top: 12,
|
||||
width: 15,
|
||||
height: 15,
|
||||
borderLeft: `1.5px solid ${tokens.accent}`,
|
||||
borderTop: `1.5px solid ${tokens.accent}`,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 12,
|
||||
bottom: 12,
|
||||
width: 15,
|
||||
height: 15,
|
||||
borderRight: `1.5px solid ${tokens.accent}`,
|
||||
borderBottom: `1.5px solid ${tokens.accent}`,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* overlay header */}
|
||||
<div
|
||||
style={{
|
||||
flex: "0 0 auto",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "16px 24px",
|
||||
borderBottom: "1px solid #cdd9e6",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "baseline", gap: 11 }}>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: 15,
|
||||
color: tokens.accent,
|
||||
}}
|
||||
>
|
||||
{overlayNums[active]}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
letterSpacing: 2.5,
|
||||
color: tokens.ink2,
|
||||
}}
|
||||
>
|
||||
{overlayTitles[active]}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
onClick={onClose}
|
||||
className="tiv2-so-back"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
cursor: "pointer",
|
||||
border: `1px solid ${tokens.line}`,
|
||||
borderRadius: 6,
|
||||
padding: "6px 12px",
|
||||
background: tokens.surface.w60,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 13 }}>←</span>
|
||||
<span
|
||||
style={{ fontSize: 10, letterSpacing: 1.5, color: tokens.muted }}
|
||||
>
|
||||
К ОЦЕНКЕ
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* overlay body */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
overflowY: "auto",
|
||||
padding: "22px 24px 26px",
|
||||
}}
|
||||
>
|
||||
{active === 1 && <HistoryView />}
|
||||
{active === 2 && <SourcesView />}
|
||||
{active === 3 && <AnalyticsView onNavigate={onNavigate} />}
|
||||
{active === 4 && <CacheView />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
401
frontend/src/components/trade-in/v2/SourcesView.tsx
Normal file
401
frontend/src/components/trade-in/v2/SourcesView.tsx
Normal file
|
|
@ -0,0 +1,401 @@
|
|||
import type { CSSProperties } from "react";
|
||||
|
||||
import { tokens } from "./tokens";
|
||||
import { adRows, dealRows, marketAds, marketDeals } from "./fixtures";
|
||||
import type { AdRow, DealRow } from "./types";
|
||||
|
||||
// Overlay 05 — РЫНОК · АНАЛОГИ И СДЕЛКИ.
|
||||
// Faithful markup port from "МЕРА Оценка.dc.html" (lines 492-537).
|
||||
// Card A: объявления в продаже (3 KPI + filter chips + adRows table).
|
||||
// Card B: фактические сделки (3 KPI + dealRows table).
|
||||
// Static markup: data from fixtures, no fetching, no interactive state.
|
||||
|
||||
// One-off tints not present in the v2 palette (kept literal, faithful to design).
|
||||
const KPI_BG = "rgba(238,244,250,.5)";
|
||||
const BADGE_BG = "#eaf1f8";
|
||||
const AVITO_DOT = "#e8743b";
|
||||
|
||||
const AD_GRID = "2.4fr 1fr 1fr 1.2fr .7fr 1fr";
|
||||
const DEAL_GRID = "2.6fr 1.2fr 1fr 1.2fr 1fr";
|
||||
|
||||
const card: CSSProperties = {
|
||||
background: tokens.surface.w55,
|
||||
border: `1px solid ${tokens.line2}`,
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
};
|
||||
|
||||
const cardHeader: CSSProperties = {
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
padding: "14px 18px",
|
||||
borderBottom: `1px solid ${tokens.lineSoft}`,
|
||||
};
|
||||
|
||||
const eyebrow: CSSProperties = {
|
||||
fontSize: "8.5px",
|
||||
letterSpacing: "2px",
|
||||
color: tokens.muted2,
|
||||
marginBottom: 5,
|
||||
};
|
||||
|
||||
const cardTitle: CSSProperties = {
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
color: tokens.ink2,
|
||||
};
|
||||
|
||||
const headerRight: CSSProperties = {
|
||||
textAlign: "right",
|
||||
fontSize: "9.5px",
|
||||
color: tokens.muted2,
|
||||
lineHeight: 1.6,
|
||||
};
|
||||
|
||||
const kpiRow: CSSProperties = {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(3,1fr)",
|
||||
borderBottom: `1px solid ${tokens.lineSoft}`,
|
||||
background: KPI_BG,
|
||||
};
|
||||
|
||||
const kpiLabel: CSSProperties = {
|
||||
fontSize: "9px",
|
||||
letterSpacing: "1.5px",
|
||||
color: tokens.muted2,
|
||||
};
|
||||
|
||||
const tableHead: CSSProperties = {
|
||||
gap: "10px",
|
||||
padding: "9px 18px",
|
||||
fontSize: "9px",
|
||||
letterSpacing: "1px",
|
||||
color: tokens.muted2,
|
||||
borderBottom: `1px solid ${tokens.lineSoft2}`,
|
||||
};
|
||||
|
||||
const tableRow: CSSProperties = {
|
||||
gap: "10px",
|
||||
padding: "11px 18px",
|
||||
fontSize: "11px",
|
||||
borderBottom: `1px solid ${tokens.lineSoft3}`,
|
||||
alignItems: "center",
|
||||
};
|
||||
|
||||
const metaSpan: CSSProperties = {
|
||||
color: tokens.muted2,
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: "9.5px",
|
||||
};
|
||||
|
||||
const badge: CSSProperties = {
|
||||
fontSize: "9px",
|
||||
background: BADGE_BG,
|
||||
border: `1px solid ${tokens.line2}`,
|
||||
borderRadius: 3,
|
||||
padding: "2px 8px",
|
||||
color: tokens.muted,
|
||||
};
|
||||
|
||||
const chip: CSSProperties = {
|
||||
fontSize: "10px",
|
||||
color: tokens.muted,
|
||||
border: `1px solid ${tokens.line2}`,
|
||||
borderRadius: 20,
|
||||
padding: "3px 10px",
|
||||
};
|
||||
|
||||
const monoMuted: CSSProperties = {
|
||||
fontFamily: tokens.font.mono,
|
||||
color: tokens.muted,
|
||||
};
|
||||
|
||||
const monoInk: CSSProperties = {
|
||||
fontFamily: tokens.font.mono,
|
||||
color: tokens.ink2,
|
||||
};
|
||||
|
||||
const monoMuted2: CSSProperties = {
|
||||
fontFamily: tokens.font.mono,
|
||||
color: tokens.muted2,
|
||||
};
|
||||
|
||||
function KpiCell({
|
||||
label,
|
||||
value,
|
||||
unit,
|
||||
valueColor = tokens.ink2,
|
||||
unitColor = tokens.muted,
|
||||
last = false,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
unit: string;
|
||||
valueColor?: string;
|
||||
unitColor?: string;
|
||||
last?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "12px 18px",
|
||||
borderRight: last ? undefined : `1px solid ${tokens.lineSoft2}`,
|
||||
}}
|
||||
>
|
||||
<div style={kpiLabel}>{label}</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: tokens.font.mono,
|
||||
fontSize: "22px",
|
||||
fontWeight: 300,
|
||||
color: valueColor,
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
{value}{" "}
|
||||
<span style={{ fontSize: "11px", color: unitColor }}>{unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SourcesView() {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 22 }}>
|
||||
<style>{`
|
||||
.sv-row { transition: background .12s ease; }
|
||||
.sv-row:hover { background: ${tokens.surface.w50}; }
|
||||
.sv-rr { transition: background .12s ease, border-color .12s ease; }
|
||||
.sv-rr:hover { background: ${BADGE_BG}; border-color: ${tokens.accent}; }
|
||||
`}</style>
|
||||
|
||||
{/* объявления */}
|
||||
<div style={card}>
|
||||
<div style={cardHeader}>
|
||||
<div>
|
||||
<div style={eyebrow}>ПОХОЖИЕ КВАРТИРЫ · В ПРОДАЖЕ</div>
|
||||
<div style={cardTitle}>
|
||||
Объявления — аналогичные квартиры в продаже
|
||||
</div>
|
||||
</div>
|
||||
<div style={headerRight}>
|
||||
Обновлено: —<br />
|
||||
Источников: <b style={{ color: tokens.muted }}>2 / 7</b>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={kpiRow}>
|
||||
<KpiCell
|
||||
label="ОБЪЯВЛЕНИЙ ПО АНАЛОГАМ"
|
||||
value={marketAds.kpi.count}
|
||||
unit="шт · из 2 источников"
|
||||
/>
|
||||
<KpiCell
|
||||
label="МЕДИАНА"
|
||||
value={marketAds.kpi.median}
|
||||
unit={`млн ₽ · ${marketAds.kpi.ppm} ₽/м²`}
|
||||
/>
|
||||
<KpiCell
|
||||
label="ДИАПАЗОН P25–P75"
|
||||
value={marketAds.kpi.range}
|
||||
unit={`млн · CV ${marketAds.kpi.cv}`}
|
||||
valueColor={tokens.accent}
|
||||
last
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
padding: "11px 18px",
|
||||
borderBottom: `1px solid ${tokens.lineSoft3}`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "9px",
|
||||
letterSpacing: "1.5px",
|
||||
color: tokens.muted2,
|
||||
}}
|
||||
>
|
||||
ФИЛЬТР
|
||||
</span>
|
||||
{marketAds.filters.map((f, i) => (
|
||||
<span key={i} style={chip}>
|
||||
{f}
|
||||
</span>
|
||||
))}
|
||||
<span
|
||||
style={{
|
||||
marginLeft: "auto",
|
||||
fontSize: "10px",
|
||||
color: tokens.muted,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 2,
|
||||
background: AVITO_DOT,
|
||||
}}
|
||||
/>
|
||||
Avito · 10
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
...tableHead,
|
||||
display: "grid",
|
||||
gridTemplateColumns: AD_GRID,
|
||||
}}
|
||||
>
|
||||
<span>АДРЕС</span>
|
||||
<span>ИСТОЧНИК</span>
|
||||
<span>₽/М²</span>
|
||||
<span>СТОИМОСТЬ</span>
|
||||
<span>ДИСТ.</span>
|
||||
<span />
|
||||
</div>
|
||||
|
||||
{adRows.map((r: AdRow, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="sv-row"
|
||||
style={{
|
||||
...tableRow,
|
||||
display: "grid",
|
||||
gridTemplateColumns: AD_GRID,
|
||||
}}
|
||||
>
|
||||
<span style={{ color: tokens.body }}>
|
||||
{r.addr}
|
||||
<span style={metaSpan}>
|
||||
{" · "}
|
||||
{r.meta}
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
<span style={badge}>Avito</span>
|
||||
</span>
|
||||
<span style={monoMuted}>{r.ppm}</span>
|
||||
<span style={monoInk}>{r.price}</span>
|
||||
<span style={monoMuted2}>—</span>
|
||||
<span
|
||||
className="sv-rr"
|
||||
style={{
|
||||
fontSize: "9.5px",
|
||||
color: tokens.accent,
|
||||
border: `1px solid ${tokens.line2}`,
|
||||
borderRadius: 4,
|
||||
padding: "3px 8px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Росреестр ↗
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* фактические сделки */}
|
||||
<div style={card}>
|
||||
<div style={cardHeader}>
|
||||
<div>
|
||||
<div style={eyebrow}>ПОХОЖИЕ КВАРТИРЫ · СДЕЛКИ</div>
|
||||
<div style={cardTitle}>
|
||||
Фактические сделки по аналогичным квартирам
|
||||
</div>
|
||||
</div>
|
||||
<div style={headerRight}>
|
||||
Период: <b style={{ color: tokens.muted }}>за 12 мес.</b>
|
||||
<br />
|
||||
проверенные данные · подтверждено Росреестром
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={kpiRow}>
|
||||
<KpiCell
|
||||
label="СДЕЛОК ПО АНАЛОГАМ"
|
||||
value={marketDeals.kpi.count}
|
||||
unit="шт · Росреестр"
|
||||
/>
|
||||
<KpiCell
|
||||
label="МЕДИАНА СДЕЛОК"
|
||||
value={marketDeals.kpi.median}
|
||||
unit={`млн ₽ · ${marketDeals.kpi.delta}`}
|
||||
unitColor={tokens.success}
|
||||
/>
|
||||
<KpiCell
|
||||
label="ДИАПАЗОН"
|
||||
value={marketDeals.kpi.range}
|
||||
unit="млн"
|
||||
valueColor={tokens.accent}
|
||||
last
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
...tableHead,
|
||||
display: "grid",
|
||||
gridTemplateColumns: DEAL_GRID,
|
||||
}}
|
||||
>
|
||||
<span>АДРЕС</span>
|
||||
<span>ИСТОЧНИК</span>
|
||||
<span>₽/М²</span>
|
||||
<span>ЦЕНА СДЕЛКИ</span>
|
||||
<span>ДАТА</span>
|
||||
</div>
|
||||
|
||||
{dealRows.map((r: DealRow, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="sv-row"
|
||||
style={{
|
||||
...tableRow,
|
||||
display: "grid",
|
||||
gridTemplateColumns: DEAL_GRID,
|
||||
}}
|
||||
>
|
||||
<span style={{ color: tokens.body }}>
|
||||
{r.addr}
|
||||
<span style={metaSpan}>
|
||||
{" · "}
|
||||
{r.meta}
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
<span style={badge}>Росреестр</span>{" "}
|
||||
<span style={{ fontSize: "9px", color: tokens.muted2 }}>
|
||||
по улице
|
||||
</span>
|
||||
</span>
|
||||
<span style={monoMuted}>{r.ppm}</span>
|
||||
<span style={monoInk}>{r.price}</span>
|
||||
<span style={monoMuted2}>01.01.26</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div
|
||||
style={{
|
||||
padding: "11px 18px",
|
||||
fontSize: "10px",
|
||||
color: tokens.hint,
|
||||
background: KPI_BG,
|
||||
}}
|
||||
>
|
||||
Показано 10 фактических сделок
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
407
frontend/src/components/trade-in/v2/TopNav.tsx
Normal file
407
frontend/src/components/trade-in/v2/TopNav.tsx
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
"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/user come from fixtures, colours from
|
||||
// tokens.
|
||||
|
||||
import { useState } from "react";
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
import { tokens } from "./tokens";
|
||||
import { navLabels, user, version } from "./fixtures";
|
||||
|
||||
interface TopNavProps {
|
||||
active: number;
|
||||
onNavigate: (i: number) => void;
|
||||
}
|
||||
|
||||
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",
|
||||
};
|
||||
|
||||
export default function TopNav({ active, onNavigate }: TopNavProps) {
|
||||
const [userOpen, setUserOpen] = useState(false);
|
||||
|
||||
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)",
|
||||
}}
|
||||
>
|
||||
{user.initials}
|
||||
</div>
|
||||
<div style={{ lineHeight: 1.25 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "12.5px",
|
||||
fontWeight: 600,
|
||||
color: tokens.ink,
|
||||
fontFamily: tokens.font.sans,
|
||||
}}
|
||||
>
|
||||
{user.name}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "9px",
|
||||
color: tokens.muted2,
|
||||
fontFamily: tokens.font.sans,
|
||||
letterSpacing: ".5px",
|
||||
}}
|
||||
>
|
||||
{user.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,
|
||||
}}
|
||||
>
|
||||
{user.initials}
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
fontWeight: 600,
|
||||
color: tokens.ink2,
|
||||
}}
|
||||
>
|
||||
{user.name}
|
||||
</div>
|
||||
<div style={{ fontSize: "10px", color: tokens.muted2 }}>
|
||||
{user.email}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="tnav-menuitem" style={menuItemStyle}>
|
||||
<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,
|
||||
}}
|
||||
>
|
||||
{user.reports}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="tnav-menuitem" style={menuItemStyle}>
|
||||
<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 className="tnav-menuitem" style={menuItemStyle}>
|
||||
<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)}
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
665
frontend/src/components/trade-in/v2/fixtures.ts
Normal file
665
frontend/src/components/trade-in/v2/fixtures.ts
Normal file
|
|
@ -0,0 +1,665 @@
|
|||
// Sample/markup data for the /trade-in/v2 "МЕРА Оценка" design port.
|
||||
// Extracted verbatim from the pixel design (МЕРА Оценка.dc.html). This is the
|
||||
// data source for the markup — components must NOT fetch; they read from here.
|
||||
// Colour fields reference tokens so the palette stays the single source of truth.
|
||||
|
||||
import { tokens } from "./tokens";
|
||||
import type {
|
||||
Analytics,
|
||||
CacheKpi,
|
||||
CacheRow,
|
||||
DkpRow,
|
||||
AdRow,
|
||||
DealRow,
|
||||
DropdownOptions,
|
||||
History,
|
||||
LocationFactors,
|
||||
MarketAds,
|
||||
MarketDeals,
|
||||
ObjectInfo,
|
||||
Ranges,
|
||||
Report,
|
||||
ResultCard,
|
||||
ResultMeta,
|
||||
ScatterMini,
|
||||
SourceCard,
|
||||
Summary,
|
||||
User,
|
||||
} from "./types";
|
||||
|
||||
export const report: Report = {
|
||||
id: "9b9e2737",
|
||||
date: "20.06.2026",
|
||||
validUntil: "21.06.2026",
|
||||
};
|
||||
|
||||
export const object: ObjectInfo = {
|
||||
address: "ул. Малышева, 30",
|
||||
city: "Екатеринбург, Россия",
|
||||
area: "54",
|
||||
rooms: "2",
|
||||
floor: "5",
|
||||
totalFloors: "17",
|
||||
year: "1985",
|
||||
houseType: "Панельный",
|
||||
repair: "Хороший",
|
||||
balcony: true,
|
||||
locationCoef: "0.87",
|
||||
streetView: "Street View · май 2024",
|
||||
compass: "СЕВЕРО-ЗАПАД",
|
||||
};
|
||||
|
||||
// ---- 02 RESULT ------------------------------------------------------------
|
||||
|
||||
export const resultCards: ResultCard[] = [
|
||||
{
|
||||
title: ["РЕКОМЕНДОВАННАЯ ЦЕНА", "В ОБЪЯВЛЕНИИ"],
|
||||
value: "11,29",
|
||||
unit: "млн ₽",
|
||||
range: "7,72 — 14,46 млн ₽",
|
||||
ppm: "225 801 ₽/м² · по объявлениям",
|
||||
bars: [30, 55, 80, 100, 70, 45, 25, 35],
|
||||
nav: 2,
|
||||
},
|
||||
{
|
||||
title: ["ОЖИДАЕМАЯ СДЕЛКА", "(ПОЛУЧИТЕ)"],
|
||||
value: "10,00",
|
||||
unit: "млн ₽",
|
||||
range: "6,83 — 12,81 млн ₽",
|
||||
ppm: "199 973 ₽/м²",
|
||||
delta: "−11%",
|
||||
deltaLabel: "К РЫНКУ",
|
||||
nav: 2,
|
||||
},
|
||||
{
|
||||
title: ["ДКП · РОСРЕЕСТР", "(ФАКТИЧЕСКИЕ СДЕЛКИ)"],
|
||||
value: "9,43",
|
||||
unit: "млн ₽",
|
||||
range: "6,10 — 12,72 млн ₽",
|
||||
ppm: "188 672 ₽/м² · 44 сделки",
|
||||
bars: [25, 45, 70, 100, 85, 55, 30, 20],
|
||||
nav: 1,
|
||||
},
|
||||
];
|
||||
|
||||
export const resultMeta: ResultMeta = {
|
||||
sources: "3 / 7",
|
||||
confidence: "СРЕДНЯЯ",
|
||||
cv: "59.7%",
|
||||
};
|
||||
|
||||
export const ranges: Ranges = {
|
||||
ads: {
|
||||
label: ["ДИАПАЗОН ЦЕН В ОБЪЯВЛЕНИЯХ", "(БЕЗ УЧЁТА РЕМОНТА)"],
|
||||
median: "11,29 млн ₽",
|
||||
lo: "7,72 млн",
|
||||
hi: "14,46 млн",
|
||||
marker: { fillLeft: "8%", fillRight: "18%", dotLeft: "58%" },
|
||||
},
|
||||
deals: {
|
||||
label: ["ДИАПАЗОН ЦЕН ПО", "ФАКТИЧЕСКИМ СДЕЛКАМ"],
|
||||
median: "6,80 млн ₽",
|
||||
lo: "4,30 млн",
|
||||
hi: "19,70 млн",
|
||||
marker: { fillLeft: "4%", fillRight: "55%", dotLeft: "33%" },
|
||||
},
|
||||
};
|
||||
|
||||
// Mini "ЦЕНА × СРОК ПРОДАЖИ" scatter inside the ranges card (viewBox 168×140).
|
||||
export const scatterMini: ScatterMini = {
|
||||
deals: [
|
||||
{ x: 52, y: 96, r: 2.4 },
|
||||
{ x: 64, y: 88, r: 2.4 },
|
||||
{ x: 58, y: 101, r: 2.4 },
|
||||
{ x: 74, y: 85, r: 2.4 },
|
||||
{ x: 47, y: 103, r: 2.4 },
|
||||
{ x: 88, y: 79, r: 2.4 },
|
||||
{ x: 70, y: 93, r: 2.4 },
|
||||
{ x: 55, y: 90, r: 2.4 },
|
||||
{ x: 82, y: 99, r: 2.4 },
|
||||
{ x: 44, y: 97, r: 2.4 },
|
||||
],
|
||||
analogs: [
|
||||
{ x: 118, y: 48, r: 3 },
|
||||
{ x: 100, y: 60, r: 3 },
|
||||
{ x: 132, y: 40, r: 3 },
|
||||
{ x: 92, y: 66, r: 3 },
|
||||
{ x: 112, y: 54, r: 3 },
|
||||
{ x: 142, y: 34, r: 3 },
|
||||
],
|
||||
subject: { x: 96, y: 70, r: 3.6 },
|
||||
yTicks: [
|
||||
{ label: "15М", y: 13 },
|
||||
{ label: "10М", y: 77 },
|
||||
{ label: "4М", y: 112 },
|
||||
],
|
||||
xTicks: [
|
||||
{ label: "0", x: 34 },
|
||||
{ label: "90", x: 96 },
|
||||
{ label: "180", x: 158 },
|
||||
],
|
||||
};
|
||||
|
||||
export const sources: SourceCard[] = [
|
||||
{ name: "ЦИАН", count: "7", label: "лотов", active: true },
|
||||
{ name: "Я.НЕДВИЖИМОСТЬ", count: "1", label: "лот", active: true },
|
||||
{ name: "РОСРЕЕСТР (ВНУТР.)", count: "10", label: "лотов", active: true },
|
||||
{ name: "АВИТО ОЦЕНКА", count: "—", label: "нет данных", active: false },
|
||||
{ name: "ДОМКЛИК ПРАЙС", count: "—", label: "нет данных", active: false },
|
||||
{ name: "RESTATE", count: "—", label: "нет данных", active: false },
|
||||
{ name: "N1.RU", count: "—", label: "нет данных", active: false },
|
||||
];
|
||||
|
||||
// ---- 03 SUMMARY -----------------------------------------------------------
|
||||
|
||||
export const summary: Summary = {
|
||||
rows: [
|
||||
{ label: "Продажи в доме", value: "2 · 32 дн", dot: "accent", nav: 1 },
|
||||
{ label: "Аналоги", value: "23 · 6,13 млн", dot: "accent", nav: 2 },
|
||||
{ label: "Сделки", value: "10 · 5,95 млн", dot: "muted", nav: 2 },
|
||||
{ label: "Аналитика", value: "67 дн · −3%", dot: "accent", nav: 3 },
|
||||
],
|
||||
quality: { sources: "3 / 7", confidence: "средняя", cv: "59.7%" },
|
||||
};
|
||||
|
||||
// ---- OVERLAY 04: ПРОДАЖИ В ДОМЕ -------------------------------------------
|
||||
|
||||
export const dkpRows: DkpRow[] = [
|
||||
{
|
||||
area: "33.9 м²",
|
||||
price: "5 950 000 ₽",
|
||||
ppm: "175 516",
|
||||
ask: "5 272 800 ₽ за 3 дн до",
|
||||
delta: "+12.8%",
|
||||
deltaColor: tokens.danger,
|
||||
},
|
||||
{
|
||||
area: "35.4 м²",
|
||||
price: "4 900 000 ₽",
|
||||
ppm: "138 418",
|
||||
ask: "5 186 700 ₽ за 3 дн до",
|
||||
delta: "−5.5%",
|
||||
deltaColor: tokens.success,
|
||||
},
|
||||
{
|
||||
area: "36.2 м²",
|
||||
price: "4 200 000 ₽",
|
||||
ppm: "116 022",
|
||||
ask: "5 186 700 ₽ за 3 дн до",
|
||||
delta: "−19.0%",
|
||||
deltaColor: tokens.success,
|
||||
},
|
||||
{
|
||||
area: "37.0 м²",
|
||||
price: "7 503 227 ₽",
|
||||
ppm: "202 790",
|
||||
ask: "5 272 800 ₽ за 3 дн до",
|
||||
delta: "+42.3%",
|
||||
deltaColor: tokens.danger,
|
||||
},
|
||||
{
|
||||
area: "36.8 м²",
|
||||
price: "7 140 243 ₽",
|
||||
ppm: "194 028",
|
||||
ask: "5 272 800 ₽ за 3 дн до",
|
||||
delta: "+35.4%",
|
||||
deltaColor: tokens.danger,
|
||||
},
|
||||
{
|
||||
area: "42.1 м²",
|
||||
price: "6 535 050 ₽",
|
||||
ppm: "155 227",
|
||||
ask: "5 272 800 ₽ за 3 дн до",
|
||||
delta: "+23.9%",
|
||||
deltaColor: tokens.danger,
|
||||
},
|
||||
{
|
||||
area: "42.0 м²",
|
||||
price: "6 650 000 ₽",
|
||||
ppm: "158 333",
|
||||
ask: "5 781 600 ₽ за 3 дн до",
|
||||
delta: "+15.0%",
|
||||
deltaColor: tokens.danger,
|
||||
},
|
||||
{
|
||||
area: "32.5 м²",
|
||||
price: "5 362 650 ₽",
|
||||
ppm: "165 005",
|
||||
ask: "5 325 600 ₽ за 3 дн до",
|
||||
delta: "+0.7%",
|
||||
deltaColor: tokens.danger,
|
||||
},
|
||||
{
|
||||
area: "38.8 м²",
|
||||
price: "4 380 000 ₽",
|
||||
ppm: "112 887",
|
||||
ask: "5 325 600 ₽ за 3 дн до",
|
||||
delta: "−17.8%",
|
||||
deltaColor: tokens.success,
|
||||
},
|
||||
{
|
||||
area: "34.9 м²",
|
||||
price: "5 000 000 ₽",
|
||||
ppm: "143 266",
|
||||
ask: "5 254 500 ₽ за 3 дн до",
|
||||
delta: "−4.8%",
|
||||
deltaColor: tokens.success,
|
||||
},
|
||||
];
|
||||
|
||||
export const history: History = {
|
||||
houseSales: [
|
||||
{
|
||||
lot: "2-к. квартира, 50 м², 6/16 эт.",
|
||||
priceFrom: "6,25",
|
||||
priceTo: "5,98 млн ₽",
|
||||
date: "11.07.2025",
|
||||
exposure: "32 дн.",
|
||||
},
|
||||
{
|
||||
lot: "2-к. квартира, 60 м², 6/16 эт.",
|
||||
priceFrom: "7,29",
|
||||
priceTo: "7,09 млн ₽",
|
||||
date: "11.07.2025",
|
||||
exposure: "31 дн.",
|
||||
},
|
||||
],
|
||||
dkpHeader: {
|
||||
title: "ДКП-сделки на улице Академика Ландау",
|
||||
period: "24 мес",
|
||||
note: "78% сделок имеют историческую цену в объявлении · медианный торг −36.0%. Данные по улице, не по дому.",
|
||||
},
|
||||
dkpKpi: { count: "95", median: "140 066", range: "3,1 – 7,5" },
|
||||
};
|
||||
|
||||
// ---- OVERLAY 05: АНАЛОГИЧНЫЕ ОБЪЯВЛЕНИЯ -----------------------------------
|
||||
|
||||
export const adRows: AdRow[] = [
|
||||
{
|
||||
addr: "ул. Академика Ландау, 39",
|
||||
meta: "32.5 м² · 1-к · этаж 12/15",
|
||||
ppm: "170 769",
|
||||
price: "5 550 000",
|
||||
},
|
||||
{
|
||||
addr: "ул. Академика Ландау, 35А/2",
|
||||
meta: "33.5 м² · 1-к · этаж 13/25",
|
||||
ppm: "174 626",
|
||||
price: "5 850 000",
|
||||
},
|
||||
{
|
||||
addr: "ул. Академика Ландау, 38",
|
||||
meta: "32.7 м² · 1-к · этаж 10/16",
|
||||
ppm: "171 253",
|
||||
price: "5 600 000",
|
||||
},
|
||||
{
|
||||
addr: "ул. Академика Ландау, 35",
|
||||
meta: "32.7 м² · 1-к · этаж 6/12",
|
||||
ppm: "178 999",
|
||||
price: "5 853 300",
|
||||
},
|
||||
{
|
||||
addr: "ул. Академика Ландау, 35",
|
||||
meta: "37.4 м² · 1-к · этаж 8/12",
|
||||
ppm: "149 732",
|
||||
price: "5 600 000",
|
||||
},
|
||||
{
|
||||
addr: "ул. Академика Парина, 29",
|
||||
meta: "40.5 м² · 1-к · этаж 22/23",
|
||||
ppm: "160 493",
|
||||
price: "6 500 000",
|
||||
},
|
||||
{
|
||||
addr: "ул. Академика Ландау, 39",
|
||||
meta: "32.5 м² · 1-к · этаж 15/15",
|
||||
ppm: "146 153",
|
||||
price: "4 750 000",
|
||||
},
|
||||
{
|
||||
addr: "ул. Академика Ландау, 39",
|
||||
meta: "34.9 м² · 1-к · этаж 9/15",
|
||||
ppm: "143 266",
|
||||
price: "5 000 000",
|
||||
},
|
||||
{
|
||||
addr: "ул. Академика Парина, 33",
|
||||
meta: "37.1 м² · 1-к · этаж 6/14",
|
||||
ppm: "160 916",
|
||||
price: "5 970 000",
|
||||
},
|
||||
{
|
||||
addr: "ул. Академика Ландау, 34",
|
||||
meta: "41.7 м² · 1-к · этаж 22/23",
|
||||
ppm: "167 865",
|
||||
price: "6 999 999",
|
||||
},
|
||||
];
|
||||
|
||||
export const dealRows: DealRow[] = [
|
||||
{
|
||||
addr: "Екатеринбург, Академика Парина",
|
||||
meta: "38.1 м² · 1-к",
|
||||
ppm: "161 417",
|
||||
price: "6 150 000",
|
||||
},
|
||||
{
|
||||
addr: "Екатеринбург, Академика Ландау",
|
||||
meta: "33.9 м² · 1-к",
|
||||
ppm: "175 516",
|
||||
price: "5 950 000",
|
||||
},
|
||||
{
|
||||
addr: "Екатеринбург, Академика Ландау",
|
||||
meta: "39.1 м² · 1-к",
|
||||
ppm: "148 338",
|
||||
price: "5 800 000",
|
||||
},
|
||||
{
|
||||
addr: "Екатеринбург, Академика Парина",
|
||||
meta: "33.8 м² · 1-к",
|
||||
ppm: "147 929",
|
||||
price: "5 000 000",
|
||||
},
|
||||
{
|
||||
addr: "Екатеринбург, Академика Сахарова",
|
||||
meta: "37.1 м² · 1-к",
|
||||
ppm: "167 116",
|
||||
price: "6 200 000",
|
||||
},
|
||||
{
|
||||
addr: "Екатеринбург, Академика Вавилова",
|
||||
meta: "38.8 м² · 1-к",
|
||||
ppm: "165 437",
|
||||
price: "6 418 972",
|
||||
},
|
||||
{
|
||||
addr: "Екатеринбург, Академика Ландау",
|
||||
meta: "42.0 м² · 1-к",
|
||||
ppm: "158 333",
|
||||
price: "6 650 000",
|
||||
},
|
||||
{
|
||||
addr: "Екатеринбург, Академика Ландау",
|
||||
meta: "34.9 м² · 1-к",
|
||||
ppm: "143 266",
|
||||
price: "5 000 000",
|
||||
},
|
||||
{
|
||||
addr: "Екатеринбург, Академика Парина",
|
||||
meta: "32.9 м² · 1-к",
|
||||
ppm: "159 574",
|
||||
price: "5 250 000",
|
||||
},
|
||||
{
|
||||
addr: "Екатеринбург, Академика Парина",
|
||||
meta: "41.2 м² · 1-к",
|
||||
ppm: "101 942",
|
||||
price: "4 200 000",
|
||||
},
|
||||
];
|
||||
|
||||
export const marketAds: MarketAds = {
|
||||
kpi: {
|
||||
count: "23",
|
||||
median: "6,13",
|
||||
ppm: "161 351",
|
||||
range: "4,7–7,5",
|
||||
cv: "45.9%",
|
||||
},
|
||||
filters: ["Расстояние ≤ 2 км", "Планировка 1-к", "Площадь 32.5 – 41.7 м²"],
|
||||
};
|
||||
|
||||
export const marketDeals: MarketDeals = {
|
||||
kpi: {
|
||||
count: "10",
|
||||
median: "5,95",
|
||||
delta: "−3% к рынку",
|
||||
range: "4,2 – 6,7",
|
||||
},
|
||||
};
|
||||
|
||||
// ---- OVERLAY 06: АНАЛИТИКА ДОМА -------------------------------------------
|
||||
|
||||
export const analytics: Analytics = {
|
||||
header: { ads: "615 объявлений", radius: "в радиусе 300 м" },
|
||||
kpis: [
|
||||
{
|
||||
label: "СРЕДНЯЯ ЭКСПОЗИЦИЯ",
|
||||
value: "67",
|
||||
unit: "дн.",
|
||||
sub: "по архивным объявлениям",
|
||||
},
|
||||
{
|
||||
label: "СРЕДНИЙ ТОРГ",
|
||||
value: "−3.0%",
|
||||
color: tokens.success,
|
||||
sub: "от начальной до итоговой цены",
|
||||
},
|
||||
{
|
||||
label: "ДОЛЯ СНЯТЫХ",
|
||||
value: "69%",
|
||||
sub: "421 из 615",
|
||||
},
|
||||
],
|
||||
sellTimeNote: "Медиана экспозиции по архивным лотам · эталон 177 394 ₽/м²",
|
||||
sellTime: [
|
||||
{
|
||||
tier: "−5% от рынка",
|
||||
days: "~63 дн.",
|
||||
range: "обычно 18–180 дн.",
|
||||
count: "69 аналогов",
|
||||
variant: "success",
|
||||
},
|
||||
{
|
||||
tier: "По медиане",
|
||||
days: "~65 дн.",
|
||||
range: "обычно 18–182 дн.",
|
||||
count: "79 аналогов",
|
||||
variant: "accent",
|
||||
},
|
||||
{
|
||||
tier: "+5%",
|
||||
days: "~78 дн.",
|
||||
range: "обычно 16–212 дн.",
|
||||
count: "61 аналог",
|
||||
variant: "gold",
|
||||
},
|
||||
{
|
||||
tier: "+10%",
|
||||
days: "~67 дн.",
|
||||
range: "обычно 26–260 дн.",
|
||||
count: "57 аналогов",
|
||||
variant: "danger",
|
||||
},
|
||||
],
|
||||
priceHistory: {
|
||||
note: "Медиана ₽/м² по годам · только этот дом · Avito + Яндекс (596 лотов)",
|
||||
years: ["2022", "2023", "2024", "2025", "2026"],
|
||||
avito: "58,75 260,82 470,69 680,69 885,73",
|
||||
yandex: "58,75 260,82 470,51 680,42 885,24",
|
||||
},
|
||||
scatterDetail: {
|
||||
note: "16 сделок и 6 аналогов в радиусе 300 м",
|
||||
deals: [
|
||||
{ x: 150, y: 210, r: 4.5 },
|
||||
{ x: 200, y: 196, r: 4.5 },
|
||||
{ x: 175, y: 222, r: 4.5 },
|
||||
{ x: 250, y: 188, r: 4.5 },
|
||||
{ x: 220, y: 204, r: 4.5 },
|
||||
{ x: 300, y: 176, r: 4.5 },
|
||||
{ x: 270, y: 200, r: 4.5 },
|
||||
{ x: 330, y: 166, r: 4.5 },
|
||||
{ x: 360, y: 182, r: 4.5 },
|
||||
{ x: 195, y: 214, r: 4.5 },
|
||||
{ x: 285, y: 190, r: 4.5 },
|
||||
{ x: 160, y: 200, r: 4.5 },
|
||||
{ x: 345, y: 198, r: 4.5 },
|
||||
{ x: 240, y: 172, r: 4.5 },
|
||||
{ x: 315, y: 186, r: 4.5 },
|
||||
{ x: 180, y: 232, r: 4.5 },
|
||||
],
|
||||
analogs: [
|
||||
{ x: 600, y: 96, r: 5.5 },
|
||||
{ x: 540, y: 124, r: 5.5 },
|
||||
{ x: 670, y: 84, r: 5.5 },
|
||||
{ x: 500, y: 142, r: 5.5 },
|
||||
{ x: 640, y: 104, r: 5.5 },
|
||||
{ x: 730, y: 70, r: 5.5 },
|
||||
],
|
||||
subject: { x: 445, y: 150, r: 6 },
|
||||
yTicks: [
|
||||
{ label: "15М", y: 24 },
|
||||
{ label: "12М", y: 81 },
|
||||
{ label: "9М", y: 138 },
|
||||
{ label: "6М", y: 195 },
|
||||
{ label: "4М", y: 252 },
|
||||
],
|
||||
xTicks: [
|
||||
{ label: "0", x: 60 },
|
||||
{ label: "45", x: 265 },
|
||||
{ label: "90", x: 470 },
|
||||
{ label: "135", x: 675 },
|
||||
{ label: "180", x: 880 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// ---- OVERLAY 07: ПРЕДЫДУЩИЕ ОЦЕНКИ ----------------------------------------
|
||||
|
||||
export const cacheRows: CacheRow[] = [
|
||||
{
|
||||
addr: "ул. Малышева, 30",
|
||||
time: "12:45:02",
|
||||
src: "3 / 7",
|
||||
status: "свежий",
|
||||
statusColor: tokens.success,
|
||||
},
|
||||
{
|
||||
addr: "ул. Куйбышева, 48",
|
||||
time: "12:31:50",
|
||||
src: "4 / 7",
|
||||
status: "свежий",
|
||||
statusColor: tokens.success,
|
||||
},
|
||||
{
|
||||
addr: "ул. Академика Ландау, 35",
|
||||
time: "11:58:14",
|
||||
src: "2 / 7",
|
||||
status: "свежий",
|
||||
statusColor: tokens.success,
|
||||
},
|
||||
{
|
||||
addr: "ул. 8 Марта, 120",
|
||||
time: "09:12:40",
|
||||
src: "3 / 7",
|
||||
status: "устаревает",
|
||||
statusColor: tokens.warn,
|
||||
},
|
||||
{
|
||||
addr: "пр. Ленина, 5",
|
||||
time: "вчера 18:04",
|
||||
src: "5 / 7",
|
||||
status: "устарел",
|
||||
statusColor: tokens.danger,
|
||||
},
|
||||
{
|
||||
addr: "ул. Белинского, 86",
|
||||
time: "вчера 14:22",
|
||||
src: "2 / 7",
|
||||
status: "устарел",
|
||||
statusColor: tokens.danger,
|
||||
},
|
||||
];
|
||||
|
||||
export const cacheKpi: CacheKpi[] = [
|
||||
{ label: "ВСЕГО ОЦЕНОК", value: "128", sub: "за последние 30 дней" },
|
||||
{
|
||||
label: "СРЕДНЯЯ ЦЕНА ОЦЕНОК",
|
||||
value: "9,6",
|
||||
unit: "млн ₽",
|
||||
sub: "по проведённым оценкам",
|
||||
},
|
||||
{ label: "ПОВТОРНЫЕ АДРЕСА", value: "18", unit: "%", sub: "доля переоценок" },
|
||||
];
|
||||
|
||||
// ---- INPUTS / DROPDOWNS ---------------------------------------------------
|
||||
|
||||
export const dropdownOptions: DropdownOptions = {
|
||||
rooms: ["1", "2", "3", "4", "5+"],
|
||||
houseType: [
|
||||
"Панельный",
|
||||
"Кирпичный",
|
||||
"Монолитный",
|
||||
"Блочный",
|
||||
"Монолитно-кирпичный",
|
||||
],
|
||||
repair: [
|
||||
"Без отделки",
|
||||
"Требует ремонта",
|
||||
"Удовлетворительный",
|
||||
"Хороший",
|
||||
"Отличный",
|
||||
],
|
||||
radius: ["300 м", "500 м", "1000 м", "2000 м"],
|
||||
crm: [
|
||||
"Не заполнять",
|
||||
"Создать сделку",
|
||||
"Привязать к контакту",
|
||||
"Экспорт в CRM",
|
||||
],
|
||||
};
|
||||
|
||||
// ---- КОЭФ. ЛОКАЦИИ DRAWER -------------------------------------------------
|
||||
|
||||
export const locationFactors: LocationFactors = {
|
||||
coef: "0.87",
|
||||
intro:
|
||||
"Показывает, как адрес корректирует стоимость относительно медианы по Екатеринбургу. 1.00 — средний уровень. 0.87 означает, что локация снижает цену примерно на 13% из-за баланса факторов ниже.",
|
||||
formula: { base: "11,29 млн", coef: "0.87", result: "9,82 млн ₽" },
|
||||
positives: [
|
||||
{ label: "Центр города, пешая доступность ключевых точек", delta: "+0.06" },
|
||||
{ label: "Транспортные узлы и остановки рядом", delta: "+0.05" },
|
||||
{ label: "Набережная и парк в 10 минутах", delta: "+0.04" },
|
||||
{ label: "Развитая торговая инфраструктура", delta: "+0.03" },
|
||||
],
|
||||
negatives: [
|
||||
{ label: "Оживлённая магистраль, шумовая нагрузка", delta: "−0.07" },
|
||||
{ label: "Дефицит парковочных мест", delta: "−0.05" },
|
||||
{ label: "Возраст жилого фонда района (1985)", delta: "−0.04" },
|
||||
{ label: "Износ инженерных сетей квартала", delta: "−0.02" },
|
||||
],
|
||||
footer:
|
||||
"Источник геоданных: OpenStreetMap POI, транспортная доступность, шумовые и экологические слои. Коэффициент пересчитывается при смене адреса.",
|
||||
};
|
||||
|
||||
// ---- NAV / CHROME ---------------------------------------------------------
|
||||
|
||||
export const navLabels: string[] = [
|
||||
"ОЦЕНКА",
|
||||
"ПРОДАЖИ В ДОМЕ",
|
||||
"АНАЛОГИЧНЫЕ ОБЪЯВЛЕНИЯ",
|
||||
"АНАЛИТИКА",
|
||||
"ПРЕДЫДУЩИЕ ОЦЕНКИ",
|
||||
];
|
||||
|
||||
export const overlayTitles: string[] = [
|
||||
"",
|
||||
"ПРОДАЖИ В ДОМЕ",
|
||||
"АНАЛОГИЧНЫЕ ОБЪЯВЛЕНИЯ",
|
||||
"АНАЛИТИКА ДОМА",
|
||||
"ПРЕДЫДУЩИЕ ОЦЕНКИ",
|
||||
];
|
||||
|
||||
export const overlayNums: string[] = ["", "04", "05", "06", "07"];
|
||||
|
||||
export const user: User = {
|
||||
name: "Андрей Петров",
|
||||
org: "Брусника",
|
||||
email: "a.petrov@mera.ru",
|
||||
initials: "АП",
|
||||
reports: 128,
|
||||
};
|
||||
|
||||
export const version = "v2.0.6";
|
||||
115
frontend/src/components/trade-in/v2/tokens.ts
Normal file
115
frontend/src/components/trade-in/v2/tokens.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
// Design tokens for the /trade-in/v2 "МЕРА Оценка" HUD/blueprint port.
|
||||
// Faithful colour palette extracted from the pixel design (МЕРА Оценка.dc.html).
|
||||
// This file is the shared colour contract for every v2 component — import from
|
||||
// here instead of hardcoding hex literals (one-off SVG stroke/opacity may stay
|
||||
// inline). NOTE: this is an isolated design port, so it intentionally does NOT
|
||||
// reuse the global GenDesign analytics tokens (different visual language).
|
||||
|
||||
// Page background behind the 1536×1024 artboard.
|
||||
export const pageBg = "#dde6ef";
|
||||
|
||||
export const tokens = {
|
||||
// Brand / accent
|
||||
accent: "#2e8bff",
|
||||
accentDeep: "#0d6fd6",
|
||||
accentLite: "#46a8ff",
|
||||
onAccent: "#fff", // opaque white text/glyph on accent-coloured surfaces (avatar initials)
|
||||
|
||||
// Ink / headings
|
||||
ink: "#1c2c40",
|
||||
ink2: "#17263a",
|
||||
|
||||
// Body text
|
||||
body: "#39506a",
|
||||
body2: "#46586c",
|
||||
|
||||
// Muted text (light → lighter)
|
||||
muted: "#6f8195",
|
||||
muted2: "#91a1b4",
|
||||
muted3: "#9aaabb",
|
||||
muted4: "#b3c0cf",
|
||||
hint: "#8a9aac", // small helper / caption text
|
||||
|
||||
// Borders & hairlines (strong → soft)
|
||||
line: "#c2d3e3",
|
||||
line2: "#c8d7e6",
|
||||
line3: "#b8c8d8", // photo/card edge, chart axes
|
||||
lineSoft: "#d3dfeb",
|
||||
lineSoft2: "#e2e9f1",
|
||||
lineSoft3: "#eef3f8",
|
||||
lineSoft4: "#eaf0f6", // pale-blue hairline — house-sales row separator (design line 462)
|
||||
lineGradient: "#cbd7e3", // decorative gradient line fade stops (footer hairline)
|
||||
|
||||
// HUD corner brackets
|
||||
bracket: "#9bbce0",
|
||||
bracket2: "#8fb4dd",
|
||||
|
||||
// Semantic
|
||||
success: "#1baa6b",
|
||||
successBorder: "#b6e0cc", // soft green border for success badges (В РАСЧЁТЕ)
|
||||
warn: "#b58a3e",
|
||||
gold: "#c79a3e",
|
||||
danger: "#cd6868",
|
||||
|
||||
// Info-soft panel (data-quality box)
|
||||
infoSoftBg: "rgba(233,242,252,.5)", // translucent blue panel fill
|
||||
infoSoftBorder: "#d3e0ec", // soft blue panel border
|
||||
surfaceTint: "rgba(238,244,250,.5)", // translucent blue-tinted surface — KPI band + table footer (design lines 471, 487)
|
||||
|
||||
// Data-viz tints (mini histograms / range bars / scatter)
|
||||
barLo: "#cfddec",
|
||||
barMid: "#a9cef5",
|
||||
track: "#dbe6f1", // range track + donut track
|
||||
scatterDeal: "#9aafc4", // grey "сделки" dots (price×time scatter charts)
|
||||
dealDot: "#8fa3b8", // grey "сделки" marker — map dot + legend (design 186, 203); darker than scatterDeal
|
||||
dotStroke: "#fff", // white outline ring around map marker dots (design 185-187)
|
||||
|
||||
// Solid backgrounds
|
||||
photoBg: "#e7eef6", // hero photo container fill behind building image
|
||||
mapBg: "#e4ebf3", // map container fill behind blueprint SVG (design 156)
|
||||
badgeTint: "#eaf1f8", // opaque pale-blue fill — source badge ("Яндекс") chip (design line 484)
|
||||
|
||||
// Disabled / no-data source card (02 RESULT · inactive source tile, design lines 366-384)
|
||||
disabledBorder: "#d8e2ec", // inactive source card edge
|
||||
disabledValue: "#c2cdd9", // greyed-out "—" count value
|
||||
disabledLabel: "#aab7c5", // greyed-out "нет данных" caption
|
||||
|
||||
// Accent glow (range-marker dot halo, design lines 303/341)
|
||||
accentGlow: "rgba(46,139,255,.5)",
|
||||
|
||||
// Translucent white surfaces (glass cards / overlays)
|
||||
surface: {
|
||||
w25: "rgba(255,255,255,.25)",
|
||||
w40: "rgba(255,255,255,.4)",
|
||||
w50: "rgba(255,255,255,.5)",
|
||||
w55: "rgba(255,255,255,.55)",
|
||||
w60: "rgba(255,255,255,.6)",
|
||||
w62: "rgba(255,255,255,.62)",
|
||||
w65: "rgba(255,255,255,.65)",
|
||||
w70: "rgba(255,255,255,.7)",
|
||||
w72: "rgba(255,255,255,.72)",
|
||||
w80: "rgba(255,255,255,.8)",
|
||||
w85: "rgba(255,255,255,.85)",
|
||||
w98: "rgba(255,255,255,.98)",
|
||||
w99: "rgba(255,255,255,.99)",
|
||||
},
|
||||
|
||||
// Font CSS variables (provided by app/trade-in/v2/layout.tsx via next/font)
|
||||
font: {
|
||||
sans: "var(--font-manrope), sans-serif",
|
||||
mono: "var(--font-plex-mono), monospace",
|
||||
},
|
||||
|
||||
// Artboard background (radial glows + blueprint grid + base gradient).
|
||||
// Ported verbatim from the design root div (lines 23-29).
|
||||
gradientBg: [
|
||||
"radial-gradient(620px 320px at 78% 16%, rgba(46,139,255,.07), transparent 60%)",
|
||||
"radial-gradient(560px 360px at 24% 64%, rgba(46,139,255,.055), transparent 60%)",
|
||||
"radial-gradient(480px 300px at 60% 92%, rgba(46,139,255,.04), transparent 60%)",
|
||||
"repeating-linear-gradient(0deg, rgba(120,150,180,.045) 0 1px, transparent 1px 38px)",
|
||||
"repeating-linear-gradient(90deg, rgba(120,150,180,.045) 0 1px, transparent 1px 38px)",
|
||||
"linear-gradient(160deg,#f4f8fc 0%,#eaf1f8 55%,#e6eef7 100%)",
|
||||
].join(", "),
|
||||
|
||||
pageBg,
|
||||
};
|
||||
284
frontend/src/components/trade-in/v2/types.ts
Normal file
284
frontend/src/components/trade-in/v2/types.ts
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
// TypeScript shapes for the /trade-in/v2 design fixtures.
|
||||
// All values are sample/markup data (see fixtures.ts) — no API types here.
|
||||
|
||||
export interface Report {
|
||||
id: string;
|
||||
date: string;
|
||||
validUntil: string;
|
||||
}
|
||||
|
||||
export interface ObjectInfo {
|
||||
address: string;
|
||||
city: string;
|
||||
area: string;
|
||||
rooms: string;
|
||||
floor: string;
|
||||
totalFloors: string;
|
||||
year: string;
|
||||
houseType: string;
|
||||
repair: string;
|
||||
balcony: boolean;
|
||||
locationCoef: string;
|
||||
streetView: string;
|
||||
compass: string;
|
||||
}
|
||||
|
||||
// ---- 02 RESULT ------------------------------------------------------------
|
||||
|
||||
export interface ResultCard {
|
||||
/** Header label, one entry per visual line. */
|
||||
title: string[];
|
||||
/** Big mono number, e.g. "11,29". */
|
||||
value: string;
|
||||
/** Unit suffix, e.g. "млн ₽". */
|
||||
unit: string;
|
||||
/** Low — high range line, e.g. "7,72 — 14,46 млн ₽". */
|
||||
range: string;
|
||||
/** ₽/м² sub-line (may include a "· N сделки" tail). */
|
||||
ppm: string;
|
||||
/** Mini-histogram column heights in % (cards 1 & 3). */
|
||||
bars?: number[];
|
||||
/** Donut delta value, e.g. "−11%" (card 2). */
|
||||
delta?: string;
|
||||
/** Donut caption, e.g. "К РЫНКУ" (card 2). */
|
||||
deltaLabel?: string;
|
||||
/** Nav index opened by the card's "Подробнее →" link. */
|
||||
nav: number;
|
||||
}
|
||||
|
||||
export interface ResultMeta {
|
||||
sources: string;
|
||||
confidence: string;
|
||||
cv: string;
|
||||
}
|
||||
|
||||
export interface RangeMarker {
|
||||
/** Filled segment offsets / dot position, as CSS % strings. */
|
||||
fillLeft: string;
|
||||
fillRight: string;
|
||||
dotLeft: string;
|
||||
}
|
||||
|
||||
export interface RangeBar {
|
||||
/** Header label, one entry per visual line. */
|
||||
label: string[];
|
||||
median: string;
|
||||
lo: string;
|
||||
hi: string;
|
||||
marker: RangeMarker;
|
||||
}
|
||||
|
||||
export interface Ranges {
|
||||
ads: RangeBar;
|
||||
deals: RangeBar;
|
||||
}
|
||||
|
||||
export interface ScatterPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
r: number;
|
||||
}
|
||||
|
||||
export interface AxisTickX {
|
||||
label: string;
|
||||
x: number;
|
||||
}
|
||||
|
||||
export interface AxisTickY {
|
||||
label: string;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface ScatterMini {
|
||||
deals: ScatterPoint[];
|
||||
analogs: ScatterPoint[];
|
||||
subject: ScatterPoint;
|
||||
yTicks: AxisTickY[];
|
||||
xTicks: AxisTickX[];
|
||||
}
|
||||
|
||||
export interface SourceCard {
|
||||
name: string;
|
||||
/** Lot count, e.g. "7" or "—" when no data. */
|
||||
count: string;
|
||||
/** Count caption: "лотов" / "лот" / "нет данных". */
|
||||
label: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
// ---- 03 SUMMARY -----------------------------------------------------------
|
||||
|
||||
export interface SummaryRow {
|
||||
label: string;
|
||||
value: string;
|
||||
dot: "accent" | "muted";
|
||||
nav: number;
|
||||
}
|
||||
|
||||
export interface SummaryQuality {
|
||||
sources: string;
|
||||
confidence: string;
|
||||
cv: string;
|
||||
}
|
||||
|
||||
export interface Summary {
|
||||
rows: SummaryRow[];
|
||||
quality: SummaryQuality;
|
||||
}
|
||||
|
||||
// ---- OVERLAY: ПРОДАЖИ В ДОМЕ / ИСТОРИЯ ------------------------------------
|
||||
|
||||
export interface DkpRow {
|
||||
area: string;
|
||||
price: string;
|
||||
ppm: string;
|
||||
ask: string;
|
||||
delta: string;
|
||||
/** Colour for the delta (success / danger token value). */
|
||||
deltaColor: string;
|
||||
}
|
||||
|
||||
export interface HouseSaleRow {
|
||||
lot: string;
|
||||
priceFrom: string;
|
||||
priceTo: string;
|
||||
date: string;
|
||||
exposure: string;
|
||||
}
|
||||
|
||||
export interface History {
|
||||
houseSales: HouseSaleRow[];
|
||||
dkpHeader: { title: string; period: string; note: string };
|
||||
dkpKpi: { count: string; median: string; range: string };
|
||||
}
|
||||
|
||||
// ---- OVERLAY: АНАЛОГИЧНЫЕ ОБЪЯВЛЕНИЯ --------------------------------------
|
||||
|
||||
export interface AdRow {
|
||||
addr: string;
|
||||
meta: string;
|
||||
ppm: string;
|
||||
price: string;
|
||||
}
|
||||
|
||||
export interface DealRow {
|
||||
addr: string;
|
||||
meta: string;
|
||||
ppm: string;
|
||||
price: string;
|
||||
}
|
||||
|
||||
export interface MarketAds {
|
||||
kpi: {
|
||||
count: string;
|
||||
median: string;
|
||||
ppm: string;
|
||||
range: string;
|
||||
cv: string;
|
||||
};
|
||||
filters: string[];
|
||||
}
|
||||
|
||||
export interface MarketDeals {
|
||||
kpi: { count: string; median: string; delta: string; range: string };
|
||||
}
|
||||
|
||||
// ---- OVERLAY: АНАЛИТИКА ДОМА ----------------------------------------------
|
||||
|
||||
export interface AnalyticsKpi {
|
||||
label: string;
|
||||
value: string;
|
||||
unit?: string;
|
||||
/** Optional value colour override (token value). */
|
||||
color?: string;
|
||||
sub: string;
|
||||
}
|
||||
|
||||
export interface SellTimeTier {
|
||||
tier: string;
|
||||
days: string;
|
||||
range: string;
|
||||
count: string;
|
||||
variant: "success" | "accent" | "gold" | "danger";
|
||||
}
|
||||
|
||||
export interface PriceHistory {
|
||||
note: string;
|
||||
years: string[];
|
||||
/** SVG polyline points string (viewBox 0 0 900 220). */
|
||||
avito: string;
|
||||
yandex: string;
|
||||
}
|
||||
|
||||
export interface ScatterDetail {
|
||||
note: string;
|
||||
deals: ScatterPoint[];
|
||||
analogs: ScatterPoint[];
|
||||
subject: ScatterPoint;
|
||||
yTicks: AxisTickY[];
|
||||
xTicks: AxisTickX[];
|
||||
}
|
||||
|
||||
export interface Analytics {
|
||||
header: { ads: string; radius: string };
|
||||
kpis: AnalyticsKpi[];
|
||||
sellTime: SellTimeTier[];
|
||||
/** Sub-line above the sell-time tiers (carries the эталон ₽/м² number). */
|
||||
sellTimeNote: string;
|
||||
priceHistory: PriceHistory;
|
||||
scatterDetail: ScatterDetail;
|
||||
}
|
||||
|
||||
// ---- OVERLAY: ПРЕДЫДУЩИЕ ОЦЕНКИ (CACHE) -----------------------------------
|
||||
|
||||
export interface CacheRow {
|
||||
addr: string;
|
||||
time: string;
|
||||
src: string;
|
||||
status: string;
|
||||
/** Status dot/text colour (success / warn / danger token value). */
|
||||
statusColor: string;
|
||||
}
|
||||
|
||||
export interface CacheKpi {
|
||||
label: string;
|
||||
value: string;
|
||||
unit?: string;
|
||||
sub: string;
|
||||
}
|
||||
|
||||
// ---- INPUTS / DROPDOWNS ---------------------------------------------------
|
||||
|
||||
export interface DropdownOptions {
|
||||
rooms: string[];
|
||||
houseType: string[];
|
||||
repair: string[];
|
||||
radius: string[];
|
||||
crm: string[];
|
||||
}
|
||||
|
||||
// ---- КОЭФ. ЛОКАЦИИ DRAWER -------------------------------------------------
|
||||
|
||||
export interface LocationFactor {
|
||||
label: string;
|
||||
delta: string;
|
||||
}
|
||||
|
||||
export interface LocationFactors {
|
||||
coef: string;
|
||||
intro: string;
|
||||
formula: { base: string; coef: string; result: string };
|
||||
positives: LocationFactor[];
|
||||
negatives: LocationFactor[];
|
||||
footer: string;
|
||||
}
|
||||
|
||||
// ---- USER / CHROME --------------------------------------------------------
|
||||
|
||||
export interface User {
|
||||
name: string;
|
||||
org: string;
|
||||
email: string;
|
||||
initials: string;
|
||||
reports: number;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue