183 lines
5.2 KiB
TypeScript
183 lines
5.2 KiB
TypeScript
"use client";
|
||
|
||
import { 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";
|
||
|
||
// ── URL persistence helpers ────────────────────────────────────────────────────
|
||
|
||
function useEstimateId() {
|
||
// SSR guard: searchParams is only available in client
|
||
if (typeof window === "undefined") return null;
|
||
const params = new URLSearchParams(window.location.search);
|
||
return params.get("id");
|
||
}
|
||
|
||
// ── Page ──────────────────────────────────────────────────────────────────────
|
||
|
||
export default function TradeInPage() {
|
||
const router = useRouter();
|
||
|
||
// Result from mutation (fresh) or from URL (restored)
|
||
const [freshResult, setFreshResult] = useState<{
|
||
estimate: AggregatedEstimate;
|
||
input: TradeInEstimateInput;
|
||
} | null>(null);
|
||
|
||
const urlEstimateId = useEstimateId();
|
||
// Fetch from URL if no fresh result yet
|
||
const restoredEstimate = useEstimate(
|
||
freshResult === null ? urlEstimateId : null,
|
||
);
|
||
|
||
const mutation = useEstimateMutation();
|
||
|
||
function handleSubmit(input: TradeInEstimateInput) {
|
||
mutation.mutate(input, {
|
||
onSuccess: (estimate) => {
|
||
setFreshResult({ estimate, input });
|
||
// 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 resultData: {
|
||
estimate: AggregatedEstimate;
|
||
input: TradeInEstimateInput;
|
||
} | null =
|
||
freshResult ??
|
||
(restoredEstimate.data
|
||
? {
|
||
estimate: restoredEstimate.data,
|
||
// When restoring from URL we don't have the original input — synthesise a minimal one
|
||
input: {
|
||
address: restoredEstimate.data.analogs[0]?.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 />
|
||
) : (
|
||
<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>
|
||
);
|
||
}
|
||
|
||
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>
|
||
);
|
||
}
|