gendesign/frontend/src/app/trade-in/page.tsx
Light1YT 86e9ea2937 fix(week-review): автофиксы код-ревью — 169 issue (label «week ревью 1»)
Многоагентный аудит + имплементация: один воркер на файл, точечные правки.
Верификация: py_compile (47/47 .py) + tsc --noEmit (0 ошибок). Unit-тесты
не прогонялись (окружение не поднято: rollup native dep / нет pytest-venv).

Полностью исправлено (169): #1336, #1337, #1339, #1340, #1341, #1342, #1343, #1345, #1346, #1348, #1349, #1350, #1351, #1354, #1356, #1358, #1359, #1360, #1362, #1364, #1365, #1366, #1367, #1368, #1369, #1370, #1371, #1372, #1373, #1374, #1375, #1376, #1377, #1378, #1379, #1380, #1381, #1382, #1384, #1385, #1386, #1387, #1388, #1389, #1390, #1391, #1392, #1394, #1395, #1396, #1397, #1399, #1400, #1401, #1402, #1403, #1404, #1408, #1409, #1410, #1411, #1412, #1413, #1414, #1415, #1416, #1417, #1418, #1420, #1423, #1425, #1426, #1427, #1428, #1429, #1430, #1431, #1432, #1433, #1434, #1435, #1437, #1438, #1439, #1440, #1441, #1442, #1443, #1444, #1445, #1446, #1447, #1448, #1449, #1450, #1451, #1452, #1453, #1454, #1455, #1456, #1457, #1458, #1459, #1460, #1461, #1462, #1463, #1464, #1465, #1466, #1467, #1468, #1469, #1471, #1472, #1473, #1474, #1476, #1478, #1479, #1481, #1482, #1483, #1484, #1485, #1487, #1488, #1489, #1490, #1491, #1492, #1493, #1494, #1495, #1496, #1497, #1499, #1500, #1501, #1502, #1504, #1505, #1506, #1507, #1510, #1514, #1515, #1516, #1517, #1518, #1519, #1521, #1522, #1523, #1524, #1525, #1526, #1527, #1528, #1529, #1531, #1532, #1533, #1534, #1535, #1536, #1537, #1538

Частично (9, in-file часть, остаток cross-file): #1361, #1419, #1422, #1424, #1470, #1475, #1477, #1480, #1498
Требуют cross-file (3, не тронуты): #1338, #1363, #1421
Пропущено (1): #1539

Не входило в партию: 22 needs-Leha issue (нужны решения владельца).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:21:11 +05:00

233 lines
7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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";
import { Suspense, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import type {
AggregatedEstimate,
TradeInEstimateInput,
} from "@/types/trade-in";
import { useEstimateMutation, useEstimate } from "@/lib/trade-in-api";
import { EstimateForm } from "@/components/trade-in/EstimateForm";
import { EstimateResult } from "@/components/trade-in/EstimateResult";
import { EstimateProgress } from "@/components/trade-in/EstimateProgress";
// ── Page ──────────────────────────────────────────────────────────────────────
function TradeInInner() {
const router = useRouter();
// App Router hook: reactive to URL changes and SSR-safe (no hydration mismatch).
const urlEstimateId = useSearchParams().get("id");
// Result from mutation (fresh) or from URL (restored)
const [freshResult, setFreshResult] = useState<{
estimate: AggregatedEstimate;
input: TradeInEstimateInput;
} | null>(null);
// Drop the fresh result when the URL points at a different estimate so a
// soft-navigation to another shared link doesn't keep showing the old one.
const [freshFor, setFreshFor] = useState<string | null>(null);
const activeFreshResult = freshFor === urlEstimateId ? freshResult : null;
// Fetch from URL if no fresh result for the current id yet
const restoredEstimate = useEstimate(
activeFreshResult === null ? urlEstimateId : null,
);
const mutation = useEstimateMutation();
function handleSubmit(input: TradeInEstimateInput) {
mutation.mutate(input, {
onSuccess: (estimate) => {
setFreshResult({ estimate, input });
// Bind the fresh result to its id so it's only shown while the URL
// still points at this estimate (see activeFreshResult).
setFreshFor(estimate.estimate_id);
// Persist estimate_id in URL for shareable link
router.replace(`/trade-in?id=${estimate.estimate_id}`, {
scroll: false,
});
},
});
}
const apiError = mutation.error?.message ?? null;
// Determine what to render on the right side
const restoreError = restoredEstimate.error?.message ?? null;
const resultData: {
estimate: AggregatedEstimate;
input: TradeInEstimateInput;
} | null =
activeFreshResult ??
(restoredEstimate.data
? {
estimate: restoredEstimate.data,
// When restoring from URL we don't have the original object parameters
// (GET /estimate/{id} returns only AggregatedEstimate, no input snapshot).
// Don't borrow the first analog's address or fabricate 0 m²/floor — those
// are someone else's listing. Pass an empty input; EstimateResult must
// hide the "Параметры объекта" block when the object params are unknown.
// TODO(TI): expose the stored input on the GET endpoint + render real params.
input: {
address: "",
area_m2: 0,
rooms: 0,
floor: 0,
total_floors: 0,
},
}
: null);
return (
<main
style={{
minHeight: "100vh",
background: "#f8f9fb",
padding: "24px 16px",
boxSizing: "border-box",
}}
>
{/* Page header */}
<div style={{ maxWidth: 1280, margin: "0 auto 20px" }}>
<h1
style={{
margin: "0 0 4px",
fontSize: 22,
fontWeight: 800,
color: "#1a1d23",
}}
>
Trade-In Estimator
</h1>
<p style={{ margin: 0, fontSize: 14, color: "#6b7280" }}>
Оценка рыночной стоимости квартиры по аналогам и сделкам ЕКБ
</p>
</div>
{/* Two-column layout */}
<div
style={{
maxWidth: 1280,
margin: "0 auto",
display: "grid",
gridTemplateColumns: "360px 1fr",
gap: 20,
alignItems: "start",
}}
className="trade-in-grid"
>
{/* Left — sticky form */}
<div style={{ position: "sticky", top: 24 }}>
<EstimateForm
onSubmit={handleSubmit}
isPending={mutation.isPending}
error={apiError}
/>
<EstimateProgress visible={mutation.isPending} />
</div>
{/* Right — result area */}
<div>
{resultData ? (
<EstimateResult
estimate={resultData.estimate}
input={resultData.input}
/>
) : restoredEstimate.isLoading ? (
<EstimateProgress visible />
) : restoreError ? (
<RestoreError />
) : (
<EmptyState />
)}
</div>
</div>
{/* Responsive: stack on mobile */}
<style>{`
@media (max-width: 767px) {
.trade-in-grid {
grid-template-columns: 1fr !important;
}
.trade-in-grid > div:first-child {
position: static !important;
}
}
`}</style>
</main>
);
}
// useSearchParams() в Next 15 App Router throw'ит при static rendering без
// <Suspense> boundary — поэтому оборачиваем page-level.
export default function TradeInPage() {
return (
<Suspense fallback={null}>
<TradeInInner />
</Suspense>
);
}
function EmptyState() {
return (
<div
style={{
padding: "48px 24px",
textAlign: "center",
color: "#9ca3af",
background: "#fff",
border: "1px dashed #e6e8ec",
borderRadius: 12,
}}
>
<div style={{ fontSize: 40, marginBottom: 12 }}>🏠</div>
<div
style={{
fontSize: 16,
fontWeight: 600,
color: "#374151",
marginBottom: 6,
}}
>
Введите параметры квартиры
</div>
<div style={{ fontSize: 14 }}>
Оценка появится здесь после нажатия «Оценить»
</div>
</div>
);
}
function RestoreError() {
return (
<div
style={{
padding: "48px 24px",
textAlign: "center",
color: "#b91c1c",
background: "#fff",
border: "1px solid #fca5a5",
borderRadius: 12,
}}
>
<div style={{ fontSize: 40, marginBottom: 12 }}></div>
<div
style={{
fontSize: 16,
fontWeight: 600,
color: "#991b1b",
marginBottom: 6,
}}
>
Ссылка устарела или не найдена
</div>
<div style={{ fontSize: 14, color: "#6b7280" }}>
Оценка по этой ссылке недоступна срок её хранения истёк. Введите
параметры квартиры слева, чтобы рассчитать новую.
</div>
</div>
);
}