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}
+
+ )}