diff --git a/app/vault/[uuid]/edit/page.tsx b/app/vault/[uuid]/edit/page.tsx index f1d5c5f..f6ad60e 100644 --- a/app/vault/[uuid]/edit/page.tsx +++ b/app/vault/[uuid]/edit/page.tsx @@ -19,6 +19,21 @@ type BuildDto = { productName?: string | null; bestPrice?: number | string | null; // backend may serialize BigDecimal as string }>; + photos?: BuildPhotoDto[]; +}; + +type BuildPhotoDto = { + uuid: string; + url: string; + caption?: string | null; + sortOrder?: number | null; + createdAt?: string | null; +}; + +type LocalPreview = { + id: string; + file: File; + url: string; // object URL }; function fmt(d?: string | null) { @@ -44,6 +59,17 @@ export default function VaultBuildEditPage() { const [error, setError] = useState(null); const [savedMsg, setSavedMsg] = useState(null); + // ✅ photo upload UI state + const [previews, setPreviews] = useState([]); + const [uploading, setUploading] = useState(false); + + // ✅ avoid object URL leaks + useEffect(() => { + return () => { + previews.forEach((p) => URL.revokeObjectURL(p.url)); + }; + }, [previews]); + const authed = !!token; // Load build once auth is ready + token exists @@ -137,6 +163,52 @@ export default function VaultBuildEditPage() { } } + function isImage(file: File) { + return file.type.startsWith("image/"); + } + + async function fileToUpload(file: File, buildUuid: string) { + // 1) ask backend for presigned upload URL + const res = await api.post( + `/api/v1/builds/me/${encodeURIComponent(buildUuid)}/photos/upload-url`, + { + fileName: file.name, + contentType: file.type, + } + ); + + if (!res.ok) { + throw new Error(await res.text().catch(() => "Failed to get upload URL")); + } + + const { uploadUrl, publicUrl } = await res.json(); + + // 2) upload directly to storage + const put = await fetch(uploadUrl, { + method: "PUT", + headers: { "Content-Type": file.type }, + body: file, + }); + + if (!put.ok) throw new Error("Upload failed"); + + // 3) confirm + create photo record + const create = await api.post( + `/api/v1/builds/me/${encodeURIComponent(buildUuid)}/photos`, + { + url: publicUrl, + caption: null, + sortOrder: 0, + } + ); + + if (!create.ok) { + throw new Error(await create.text().catch(() => "Failed to save photo")); + } + + return (await create.json()) as BuildPhotoDto; + } + async function onDelete() { if (!uuid) return; @@ -346,6 +418,170 @@ export default function VaultBuildEditPage() { + {/* Build Photos */} +
+
+
+
Build Photos
+
+ Select photos, remove any you don’t want, then upload. +
+
+ +
+ {/* Select (no upload) */} + + + {/* Upload (real upload) */} + +
+
+ + {/* Gallery */} +
+ {/* Local previews (pending upload) */} + {previews.map((p) => ( +
+ {/* remove X */} + + + Pending upload +
+ {uploading ? "Uploading…" : "Pending upload"} +
+
+ ))} + + {/* Uploaded photos */} + {(build?.photos ?? []).map((p) => ( +
+ {p.caption +
+ {p.caption ?? "—"} +
+
+ ))} + + {previews.length === 0 && (build?.photos?.length ?? 0) === 0 && ( +
+ No photos yet. Select a few and upload when ready. +
+ )} +
+
+ {/* danger zone */}
diff --git a/app/vault/page.tsx b/app/vault/page.tsx index 98ff616..833da9f 100644 --- a/app/vault/page.tsx +++ b/app/vault/page.tsx @@ -226,7 +226,7 @@ export default function VaultPage() { }`} title={valid ? "Load this build into the builder" : "Invalid build id"} > - Open + View in Builder - Edit + Edit Title