"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(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 (
{/* Page header */}

Trade-In Estimator

Оценка рыночной стоимости квартиры по аналогам и сделкам ЕКБ

{/* Two-column layout */}
{/* Left — sticky form */}
{/* Right — result area */}
{resultData ? ( ) : restoredEstimate.isLoading ? ( ) : restoreError ? ( ) : ( )}
{/* Responsive: stack on mobile */}
); } // useSearchParams() в Next 15 App Router throw'ит при static rendering без // boundary — поэтому оборачиваем page-level. export default function TradeInPage() { return ( ); } function EmptyState() { return (
🏠
Введите параметры квартиры
Оценка появится здесь после нажатия «Оценить»
); } function RestoreError() { return (
⚠️
Ссылка устарела или не найдена
Оценка по этой ссылке недоступна — срок её хранения истёк. Введите параметры квартиры слева, чтобы рассчитать новую.
); }