diff --git a/src/app/api/circles/[id]/posts/route.ts b/src/app/api/circles/[id]/posts/route.ts index d4e9878..ceb344c 100644 --- a/src/app/api/circles/[id]/posts/route.ts +++ b/src/app/api/circles/[id]/posts/route.ts @@ -110,20 +110,37 @@ export async function POST( // ── Sub-action: get presigned upload URL for a circle post image ────────── if (body.action === "presign") { - const { contentType, filename } = body; - const ALLOWED = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/heic"]; - if (!contentType || !ALLOWED.includes(contentType)) { - return NextResponse.json({ error: "Unsupported file type" }, { status: 400 }); + try { + const { filename } = body; + let { contentType } = body as { contentType?: string }; + + // Normalise empty/missing content type by sniffing the extension + if (!contentType) { + const ext = (filename ?? "").split(".").pop()?.toLowerCase() ?? ""; + const EXT_MAP: Record = { + jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", + webp: "image/webp", heic: "image/heic", heif: "image/heif", gif: "image/gif", + }; + contentType = EXT_MAP[ext] ?? ""; + } + + const ALLOWED = ["image/jpeg", "image/jpg", "image/png", "image/webp", + "image/heic", "image/heif", "image/gif"]; + if (!ALLOWED.includes(contentType)) { + return NextResponse.json({ error: `Unsupported file type: ${contentType || "unknown"}` }, { status: 400 }); + } + + const ext = filename?.split(".").pop()?.toLowerCase() ?? "jpg"; + const tmpKey = `circle-posts/tmp/${familyId}/${Date.now()}.${ext}`; + const uploadUrl = await getSignedUrl( + makeR2Client(), + new PutObjectCommand({ Bucket: process.env.R2_BUCKET_NAME!, Key: tmpKey, ContentType: contentType }), + { expiresIn: 3600 } + ); + return NextResponse.json({ uploadUrl, tmpKey, publicUrl: `${R2_PUBLIC_URL}/${tmpKey}`, contentType }); + } catch (err: unknown) { + return NextResponse.json({ error: err instanceof Error ? err.message : String(err) }, { status: 500 }); } - const ext = filename?.split(".").pop()?.toLowerCase() ?? "jpg"; - // Temp key — will be moved to circle-posts/{post_id}/ after post is created - const tmpKey = `circle-posts/tmp/${familyId}/${Date.now()}.${ext}`; - const uploadUrl = await getSignedUrl( - makeR2Client(), - new PutObjectCommand({ Bucket: process.env.R2_BUCKET_NAME!, Key: tmpKey, ContentType: contentType }), - { expiresIn: 3600 } - ); - return NextResponse.json({ uploadUrl, tmpKey, publicUrl: `${R2_PUBLIC_URL}/${tmpKey}` }); } // ── Sub-action: share from a private memory (copy-on-share) ────────────── diff --git a/src/app/circle/[id]/page.tsx b/src/app/circle/[id]/page.tsx index 0e50457..736dd55 100644 --- a/src/app/circle/[id]/page.tsx +++ b/src/app/circle/[id]/page.tsx @@ -211,41 +211,66 @@ function CreatePostModal({ const [imagePreview, setImagePreview] = useState(null); const [posting, setPosting] = useState(false); const [step, setStep] = useState<"compose" | "confirm">("compose"); + const [postError, setPostError] = useState(null); const pickImage = (e: React.ChangeEvent) => { const f = e.target.files?.[0]; if (!f) return; setImageFile(f); setImagePreview(URL.createObjectURL(f)); + setPostError(null); }; const submit = async () => { setPosting(true); + setPostError(null); try { let tmpKey: string | null = null; if (imageFile) { - // Get presigned URL + // Get presigned URL — pass both type and name so server can normalise empty types const presignRes = await fetch(`/api/circles/${circleId}/posts`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "presign", contentType: imageFile.type, filename: imageFile.name }), }); - const { uploadUrl, tmpKey: key } = await presignRes.json(); - // Upload to R2 - await fetch(uploadUrl, { method: "PUT", body: imageFile, headers: { "Content-Type": imageFile.type } }); - tmpKey = key; + const presignData = await presignRes.json(); + if (!presignRes.ok || !presignData.uploadUrl) { + setPostError(presignData.error ?? "Could not prepare image upload"); + setPosting(false); + return; + } + // Upload directly to R2 using the normalised content type from server + const uploadRes = await fetch(presignData.uploadUrl, { + method: "PUT", + body: imageFile, + headers: { "Content-Type": presignData.contentType ?? imageFile.type }, + }); + if (!uploadRes.ok) { + setPostError("Image upload failed — please try again"); + setPosting(false); + return; + } + tmpKey = presignData.tmpKey; } - await fetch(`/api/circles/${circleId}/posts`, { + const postRes = await fetch(`/api/circles/${circleId}/posts`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ postBody: body, tmpKey }), }); + const postData = await postRes.json(); + if (!postRes.ok) { + setPostError(postData.error ?? "Failed to create post"); + setPosting(false); + return; + } onPosted(); onClose(); - } catch { /* silent */ } + } catch (err) { + setPostError(err instanceof Error ? err.message : "Something went wrong"); + } setPosting(false); }; @@ -302,6 +327,11 @@ function CreatePostModal({

{body &&

"{body}"

} + {postError && ( +

+ ✗ {postError} +

+ )}