gendesign/frontend/src/app/trade-in/page.tsx
lekss361 90eb5af883
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / deploy (push) Successful in 39s
Deploy / build-frontend (push) Successful in 2m45s
feat(trade-in): TI-3 frontend /trade-in page + 5 components + hooks (#317)
2026-05-17 16:58:53 +00:00

183 lines
5.2 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";
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>
);
}