Merge pull request 'feat(tradein): фронтенд загрузки фото квартиры (#394)' (#408) from feat/tradein-photo-upload-ui into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 4s
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 1m24s
Deploy Trade-In / deploy (push) Successful in 17s

This commit is contained in:
lekss361 2026-05-22 06:11:05 +00:00
commit 88d2840edf
2 changed files with 132 additions and 0 deletions

View file

@ -14,6 +14,7 @@ import { useEstimateMutation, useEstimate } from "@/lib/trade-in-api";
import { EstimateForm } from "@/components/trade-in/EstimateForm";
import { SourcesProgress } from "@/components/trade-in/SourcesProgress";
import { HeroSummary } from "@/components/trade-in/HeroSummary";
import { PhotoUpload } from "@/components/trade-in/PhotoUpload";
import { ListingsCard } from "@/components/trade-in/ListingsCard";
import { DealsCard } from "@/components/trade-in/DealsCard";
import { OfferCard } from "@/components/trade-in/OfferCard";
@ -155,6 +156,7 @@ export default function TradeInPage() {
{resultData ? (
<>
<HeroSummary estimate={resultData.estimate} input={resultData.input} />
<PhotoUpload estimateId={resultData.estimate.estimate_id} />
<ListingsCard estimate={resultData.estimate} />
<DealsCard estimate={resultData.estimate} />
<OfferCard estimate={resultData.estimate} />

View file

@ -0,0 +1,130 @@
"use client";
/* Загрузка фото квартиры к оценке (#394). */
/* eslint-disable @next/next/no-img-element -- фото отдаёт API, next/image тут лишний */
import { useCallback, useEffect, useRef, useState } from "react";
import { API_BASE_URL } from "@/lib/api";
import { getOrCreateSessionId } from "@/lib/sessionId";
const BASE = "/api/v1/trade-in";
const MAX_PHOTOS = 12;
interface PhotoMeta {
id: string;
filename: string | null;
content_type: string;
size_bytes: number | null;
uploaded_at: string;
}
export function PhotoUpload({ estimateId }: { estimateId: string }) {
const [photos, setPhotos] = useState<PhotoMeta[]>([]);
const [uploading, setUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
const refresh = useCallback(async () => {
try {
const r = await fetch(`${API_BASE_URL}${BASE}/estimate/${estimateId}/photos`);
if (r.ok) setPhotos((await r.json()) as PhotoMeta[]);
} catch {
/* список фото некритичен — молча игнорируем */
}
}, [estimateId]);
useEffect(() => {
void refresh();
}, [refresh]);
async function handleFiles(files: FileList | null) {
if (!files || files.length === 0) return;
setUploading(true);
setError(null);
const sid = getOrCreateSessionId();
for (const file of Array.from(files)) {
const form = new FormData();
form.append("file", file);
try {
const r = await fetch(
`${API_BASE_URL}${BASE}/estimate/${estimateId}/photos`,
{
method: "POST",
body: form,
headers: sid ? { "X-Session-Id": sid } : {},
},
);
if (!r.ok) setError(`${file.name}: ошибка загрузки (${r.status})`);
} catch {
setError(`${file.name}: сеть недоступна`);
}
}
setUploading(false);
if (inputRef.current) inputRef.current.value = "";
await refresh();
}
const full = photos.length >= MAX_PHOTOS;
return (
<article className="card">
<div className="card-body">
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 12,
}}
>
<h3 style={{ fontSize: 15, fontWeight: 600 }}>Фото квартиры</h3>
<span style={{ color: "var(--muted)", fontSize: 13 }}>
{photos.length} / {MAX_PHOTOS}
</span>
</div>
<input
ref={inputRef}
type="file"
accept="image/*"
multiple
disabled={uploading || full}
onChange={(e) => handleFiles(e.target.files)}
/>
{uploading && (
<p style={{ color: "var(--muted)", fontSize: 13, marginTop: 8 }}>
Загрузка
</p>
)}
{full && !uploading && (
<p style={{ color: "var(--muted)", fontSize: 13, marginTop: 8 }}>
Достигнут лимит {MAX_PHOTOS} фото.
</p>
)}
{error && (
<p style={{ color: "#c0392b", fontSize: 13, marginTop: 8 }}>{error}</p>
)}
{photos.length > 0 && (
<div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginTop: 12 }}>
{photos.map((p) => (
<img
key={p.id}
src={`${API_BASE_URL}${BASE}/estimate/${estimateId}/photos/${p.id}`}
alt={p.filename ?? "фото квартиры"}
style={{
width: 96,
height: 96,
objectFit: "cover",
borderRadius: 8,
border: "1px solid var(--border, #e2e2e2)",
}}
/>
))}
</div>
)}
</div>
</article>
);
}