- FastAPI backend: PostGIS estimator + 3 scrapers (Avito/Cian/Yandex)
- Next.js 15 frontend: tradein.html mockup design, basePath=/trade-in
- WeasyPrint PDF (Брусника-style 4-page report)
- Address autocomplete с typo-tolerance + 6 EKB presets
- Изолированный docker stack gendesign-tradein (отдельная postgres БД)
- Caddy inline routes: gendsgn.ru/trade-in/* и /trade-in/api/v1/*
- Forgejo Actions: .forgejo/workflows/deploy-tradein.yml (shell-based GHCR login)
- Триггер только по paths: tradein-mvp/** (не пересекается с deploy.yml)
- Образы: ghcr.io/lekss361/gendesign-tradein-{backend,frontend}:latest
Первый запуск на сервере (вручную, один раз):
- создать /opt/gendesign/tradein-mvp/.env.runtime (postgres pwd, contact email)
- docker network create gendesign_shared (если нет)
- docker compose -p gendesign-tradein up -d
- docker compose -p gendesign exec caddy caddy reload
78 lines
1.8 KiB
TypeScript
78 lines
1.8 KiB
TypeScript
"use client";
|
|
|
|
interface Props {
|
|
rangeLow: number;
|
|
rangeHigh: number;
|
|
median: number;
|
|
}
|
|
|
|
function fmtRub(value: number): string {
|
|
return value.toLocaleString("ru-RU", {
|
|
style: "currency",
|
|
currency: "RUB",
|
|
maximumFractionDigits: 0,
|
|
});
|
|
}
|
|
|
|
export function PriceRangeBar({ rangeLow, rangeHigh, median }: Props) {
|
|
const span = rangeHigh - rangeLow;
|
|
// Guard against degenerate case
|
|
const medianPct =
|
|
span > 0
|
|
? Math.max(0, Math.min(100, ((median - rangeLow) / span) * 100))
|
|
: 50;
|
|
|
|
return (
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
{/* Bar */}
|
|
<div
|
|
style={{
|
|
position: "relative",
|
|
height: 10,
|
|
borderRadius: 999,
|
|
background: "linear-gradient(to right, #bfdbfe, #1d4ed8)",
|
|
}}
|
|
>
|
|
{/* Median marker */}
|
|
<div
|
|
title={`Медиана: ${fmtRub(median)}`}
|
|
style={{
|
|
position: "absolute",
|
|
top: "50%",
|
|
left: `${medianPct}%`,
|
|
transform: "translate(-50%, -50%)",
|
|
width: 14,
|
|
height: 14,
|
|
borderRadius: "50%",
|
|
background: "#1d4ed8",
|
|
border: "2px solid #fff",
|
|
boxShadow: "0 1px 4px rgba(0,0,0,0.25)",
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Labels */}
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
justifyContent: "space-between",
|
|
fontSize: 12,
|
|
color: "#5b6066",
|
|
fontVariantNumeric: "tabular-nums",
|
|
}}
|
|
>
|
|
<span>{fmtRub(rangeLow)}</span>
|
|
<span
|
|
style={{
|
|
fontWeight: 700,
|
|
color: "#1d4ed8",
|
|
fontSize: 13,
|
|
}}
|
|
>
|
|
{fmtRub(median)}
|
|
</span>
|
|
<span>{fmtRub(rangeHigh)}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|