diff --git a/tradein-mvp/frontend/src/app/page.tsx b/tradein-mvp/frontend/src/app/page.tsx index 4b2d2d0e..8f12f03b 100644 --- a/tradein-mvp/frontend/src/app/page.tsx +++ b/tradein-mvp/frontend/src/app/page.tsx @@ -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 ? ( <> + diff --git a/tradein-mvp/frontend/src/components/trade-in/PhotoUpload.tsx b/tradein-mvp/frontend/src/components/trade-in/PhotoUpload.tsx new file mode 100644 index 00000000..0e1b3c2a --- /dev/null +++ b/tradein-mvp/frontend/src/components/trade-in/PhotoUpload.tsx @@ -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([]); + const [uploading, setUploading] = useState(false); + const [error, setError] = useState(null); + const inputRef = useRef(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 ( +
+
+
+

Фото квартиры

+ + {photos.length} / {MAX_PHOTOS} + +
+ + handleFiles(e.target.files)} + /> + + {uploading && ( +

+ Загрузка… +

+ )} + {full && !uploading && ( +

+ Достигнут лимит {MAX_PHOTOS} фото. +

+ )} + {error && ( +

{error}

+ )} + + {photos.length > 0 && ( +
+ {photos.map((p) => ( + {p.filename + ))} +
+ )} +
+
+ ); +}