"use client"; import { useState, useEffect, useCallback, use } from "react"; import { useRouter } from "next/navigation"; import { useFamily } from "../../FamilyProvider"; import type { CirclePost, CircleComment, Circle } from "@/types"; const REACTIONS = ["โค๏ธ", "๐Ÿ˜‚", "๐Ÿ‘", "๐Ÿ™", "๐Ÿ˜ฎ"]; function timeAgo(iso: string) { const diff = Date.now() - new Date(iso).getTime(); const m = Math.floor(diff / 60000); if (m < 1) return "just now"; if (m < 60) return `${m}m`; const h = Math.floor(m / 60); if (h < 24) return `${h}h`; return `${Math.floor(h / 24)}d`; } // โ”€โ”€ Post card โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ function PostCard({ post, myFamilyId, circleId, isAdmin, onDeleted, onReact, }: { post: CirclePost; myFamilyId: string; circleId: string; isAdmin: boolean; onDeleted: (id: string) => void; onReact: (postId: string, emoji: string) => void; }) { const [showComments, setShowComments] = useState(false); const [comments, setComments] = useState([]); const [commentText, setCommentText] = useState(""); const [posting, setPosting] = useState(false); const [showMenu, setShowMenu] = useState(false); const [reportSent, setReportSent] = useState(false); const [deleteConfirm, setDeleteConfirm] = useState(false); const loadComments = async () => { const res = await fetch(`/api/circles/${circleId}/posts/${post.id}/comments`); const data = await res.json(); setComments(data.comments ?? []); }; const toggleComments = () => { if (!showComments) loadComments(); setShowComments(v => !v); }; const addComment = async () => { if (!commentText.trim()) return; setPosting(true); await fetch(`/api/circles/${circleId}/posts/${post.id}/comments`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ body: commentText.trim() }), }); setCommentText(""); loadComments(); setPosting(false); }; const sendReport = async (reason: string) => { await fetch(`/api/circles/${circleId}/posts/${post.id}/report`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ reason }), }); setReportSent(true); setShowMenu(false); }; const deletePost = async () => { await fetch(`/api/circles/${circleId}/posts/${post.id}`, { method: "DELETE" }); onDeleted(post.id); }; const isOwn = post.authorFamilyId === myFamilyId; return (
{/* Author row */}
๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ง

{post.authorFamilyName}

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

{/* โ‹ฏ menu */}
{showMenu && ( <>
setShowMenu(false)} />
{(isOwn || isAdmin) && ( )} {!isOwn && !reportSent && ( )} {reportSent &&

Reported โ€” thank you

}
)}
{/* Delete confirmation */} {deleteConfirm && (

Delete this post?

)} {/* Body */} {post.body &&

{post.body}

} {/* Image */} {post.imageUrl && ( Post )} {/* Reactions */}
{REACTIONS.map(emoji => { const r = post.reactions.find(x => x.emoji === emoji); return ( ); })}
{/* Comments */} {showComments && (
{comments.map(c => (
๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ง

{c.authorFamilyName}

{c.body}

))}
setCommentText(e.target.value)} onKeyDown={e => e.key === "Enter" && addComment()} placeholder="Add a commentโ€ฆ" className="flex-1 px-3 py-2 bg-gray-50 dark:bg-gray-700 rounded-xl text-sm outline-none" />
)}
); } // โ”€โ”€ Create Post Modal (C5 + C9 consent) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ function CreatePostModal({ circleId, circleName, memberCount, onClose, onPosted, }: { circleId: string; circleName: string; memberCount: number; onClose: () => void; onPosted: () => void; }) { const [body, setBody] = useState(""); const [imageFile, setImageFile] = useState(null); 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 โ€” 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 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; } 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 (err) { setPostError(err instanceof Error ? err.message : "Something went wrong"); } setPosting(false); }; return ( <>

{step === "confirm" ? "Confirm post" : "New post"}

{step === "compose" ? ( <>