Прод-/trade-in/* обслуживает tradein-mvp/frontend (basePath=/trade-in, standalone), а не главный frontend — v2 по ошибке попал в главный, где прод его не отдаёт. Markup-only порт (fixtures, без API): - компоненты v2 → tradein-mvp/frontend/src/components/trade-in/v2/ - роут src/app/v2/ (basePath даёт прод-URL /trade-in/v2) - public/trade-in-v2/building.png - HeroBar: <img> → next/image (плоский img не префиксит basePath → 404) - удалён мёртвый v2 из главного frontend next build (NEXT_PUBLIC_BASE_PATH=/trade-in) зелёный, роут /v2 prerendered.
401 lines
10 KiB
TypeScript
401 lines
10 KiB
TypeScript
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>
|
||
);
|
||
}
|