From 581fdb074df4cda967ec4b1b35ef3fca0bc47f25 Mon Sep 17 00:00:00 2001 From: Mannu Date: Sun, 24 May 2026 07:56:10 +0530 Subject: [PATCH] fix: post menu clipping, add edit post, fix image display + fullscreen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove overflow-hidden from PostCard root so the ⋯ dropdown menu is no longer clipped by the card boundary - Add Edit Post option to menu (own posts only) with inline textarea and Save/Cancel; calls new PATCH /api/circles/[id]/posts/[postId] - Add PATCH endpoint: author-only text edit - Fix image display: object-contain (no crop) instead of object-cover - Add tap-to-fullscreen lightbox: clicking any post image opens a full-screen black overlay with the image at natural size, ✕ to close Co-Authored-By: Claude Sonnet 4.6 --- .../api/circles/[id]/posts/[postId]/route.ts | 30 +++++++ src/app/circle/[id]/page.tsx | 83 +++++++++++++++++-- 2 files changed, 105 insertions(+), 8 deletions(-) diff --git a/src/app/api/circles/[id]/posts/[postId]/route.ts b/src/app/api/circles/[id]/posts/[postId]/route.ts index 7b4f909..d1d93c6 100644 --- a/src/app/api/circles/[id]/posts/[postId]/route.ts +++ b/src/app/api/circles/[id]/posts/[postId]/route.ts @@ -3,6 +3,36 @@ import { sql } from "@/db"; import { requireFamily } from "@/lib/auth"; import { S3Client, DeleteObjectCommand } from "@aws-sdk/client-s3"; +// PATCH — author can edit the text body of their own post +export async function PATCH( + req: Request, + { params }: { params: Promise<{ id: string; postId: string }> } +) { + try { + const auth = await requireFamily(); + if (!auth.success) return NextResponse.json({ error: auth.error }, { status: auth.status }); + + const familyId = auth.session!.familyId!; + const { id: circleId, postId } = await params; + + const [post] = await sql.unsafe( + `SELECT author_family_id as "authorFamilyId" FROM circle_posts WHERE id = $1 AND circle_id = $2`, + [postId, circleId] + ); + if (!post) return NextResponse.json({ error: "Post not found" }, { status: 404 }); + if (post.authorFamilyId !== familyId) + return NextResponse.json({ error: "Only the author can edit this post" }, { status: 403 }); + + const body = await req.json(); + const newBody = (body.body ?? "").trim(); + + await sql.unsafe(`UPDATE circle_posts SET body = $1 WHERE id = $2`, [newBody || null, postId]); + return NextResponse.json({ success: true }); + } catch (err: unknown) { + return NextResponse.json({ error: err instanceof Error ? err.message : String(err) }, { status: 500 }); + } +} + function makeR2Client() { return new S3Client({ region: "auto", diff --git a/src/app/circle/[id]/page.tsx b/src/app/circle/[id]/page.tsx index 3901208..ea934d0 100644 --- a/src/app/circle/[id]/page.tsx +++ b/src/app/circle/[id]/page.tsx @@ -36,6 +36,10 @@ function PostCard({ const [showMenu, setShowMenu] = useState(false); const [reportSent, setReportSent] = useState(false); const [deleteConfirm, setDeleteConfirm] = useState(false); + const [editMode, setEditMode] = useState(false); + const [editBody, setEditBody] = useState(post.body ?? ""); + const [savingEdit, setSavingEdit] = useState(false); + const [lightbox, setLightbox] = useState(false); const loadComments = async () => { const res = await fetch(`/api/circles/${circleId}/posts/${post.id}/comments`); @@ -76,10 +80,38 @@ function PostCard({ onDeleted(post.id); }; + const saveEdit = async () => { + setSavingEdit(true); + await fetch(`/api/circles/${circleId}/posts/${post.id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ body: editBody }), + }); + post.body = editBody; // optimistic local update + setEditMode(false); + setSavingEdit(false); + }; + const isOwn = post.authorFamilyId === myFamilyId; return ( -
+ <> + {/* Fullscreen image lightbox */} + {lightbox && post.imageUrl && ( +
setLightbox(false)} + > + Full size + +
+ )} + +
{/* Author row */}
@@ -89,13 +121,19 @@ function PostCard({

{timeAgo(post.createdAt)}{post.sourceKind ? ` · shared a ${post.sourceKind}` : ""}

- {/* ⋯ menu */} + {/* ⋯ menu — rendered outside overflow:hidden so it doesn't get clipped */}
- + {showMenu && ( <>
setShowMenu(false)} /> -
+
+ {isOwn && ( + + )} {(isOwn || isAdmin) && (
)} - {/* Body */} - {post.body &&

{post.body}

} + {/* Body — inline edit or display */} + {editMode ? ( +
+