gendesign/tradein-mvp/frontend/src/components/trade-in/TestPresets.tsx
lekss361 02267d0306 feat: add tradein-mvp subproject (Trade-In Estimator под /trade-in)
- 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
2026-05-21 00:25:39 +03:00

185 lines
5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

"use client";
/**
* TestPresets — кнопки быстрого заполнения формы тестовыми квартирами.
* Все эти квартиры дают medium / high confidence (хорошее покрытие в DB).
*/
import type { TradeInEstimateInput } from "@/types/trade-in";
interface Preset {
label: string;
hint: string;
data: TradeInEstimateInput;
}
const PRESETS: Preset[] = [
{
label: "Малышева 84 · 2к/55м²",
hint: "Центр · 36 аналогов",
data: {
address: "Екатеринбург, ул. Малышева, 84",
area_m2: 55,
rooms: 2,
floor: 4,
total_floors: 9,
year_built: 1990,
house_type: "brick",
},
},
{
label: "Куйбышева 48 · 3к/75м²",
hint: "Парковый · 22 аналога",
data: {
address: "Екатеринбург, ул. Куйбышева, 48",
area_m2: 75,
rooms: 3,
floor: 7,
total_floors: 12,
year_built: 2005,
house_type: "monolith",
},
},
{
label: "Цвиллинга 58 · 1к/46м²",
hint: "Гольфстрим · 17 аналогов",
data: {
address: "Екатеринбург, улица Цвиллинга, 58",
area_m2: 46,
rooms: 1,
floor: 4,
total_floors: 25,
year_built: 2020,
house_type: "monolith",
},
},
{
label: "8 Марта 50 · 1к/45м²",
hint: "Ленинский · 19 аналогов",
data: {
address: "Екатеринбург, ул. 8 Марта, 50",
area_m2: 45,
rooms: 1,
floor: 3,
total_floors: 9,
year_built: 1985,
house_type: "brick",
},
},
{
label: "Малышева 84 · 3к/68м²",
hint: "Центр · 33 аналога",
data: {
address: "Екатеринбург, ул. Малышева, 84",
area_m2: 68,
rooms: 3,
floor: 5,
total_floors: 9,
year_built: 1980,
house_type: "brick",
},
},
{
label: "Белинского 200 · 2к/50м²",
hint: "ЮЗ · 7 аналогов · премиум",
data: {
address: "Екатеринбург, ул. Белинского, 200",
area_m2: 50,
rooms: 2,
floor: 6,
total_floors: 10,
year_built: 1990,
house_type: "panel",
},
},
];
interface Props {
onPick: (data: TradeInEstimateInput) => void;
}
export function TestPresets({ onPick }: Props) {
return (
<div className="test-presets">
<div className="test-presets-label">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
</svg>
<span>Тестовые квартиры</span>
<span className="hint">кликни заполнится форма</span>
</div>
<div className="test-presets-grid">
{PRESETS.map((p, i) => (
<button
key={i}
type="button"
className="preset-chip"
onClick={() => onPick(p.data)}
>
<span className="preset-label">{p.label}</span>
<span className="preset-hint">{p.hint}</span>
</button>
))}
</div>
<style>{`
.test-presets {
margin-top: 16px;
padding: 12px 14px;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius);
}
.test-presets-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
color: var(--fg-2);
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
margin-bottom: 10px;
}
.test-presets-label svg { color: var(--accent-2); }
.test-presets-label .hint {
font-weight: 400;
color: var(--muted-2);
text-transform: none;
letter-spacing: 0;
font-size: 11px;
margin-left: auto;
}
.test-presets-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 8px;
}
.preset-chip {
display: flex;
flex-direction: column;
gap: 2px;
padding: 8px 10px;
background: var(--surface);
border: 1px solid var(--border-2);
border-radius: var(--radius-sm);
cursor: pointer;
text-align: left;
font: 500 12px var(--font-sans);
color: var(--fg);
transition: border-color .12s, background .12s;
}
.preset-chip:hover {
border-color: var(--accent);
background: var(--accent-soft);
}
.preset-label {
font-weight: 600;
color: var(--fg);
}
.preset-hint {
font-size: 11px;
color: var(--muted);
}
`}</style>
</div>
);
}