feat(tradein): фронтенд загрузки фото квартиры (#394) #408
2 changed files with 132 additions and 0 deletions
|
|
@ -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} />
|
||||
|
|
|
|||
130
tradein-mvp/frontend/src/components/trade-in/PhotoUpload.tsx
Normal file
130
tradein-mvp/frontend/src/components/trade-in/PhotoUpload.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue