import React, { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { loadAuth } from "../../auth/authStorage"; import { fetchPostLikeState, recordPostShare, togglePostLike, updatePostVisibility, fetchProfile, editPost, deletePost, } from "../../api/client"; import { useAuth } from "../../auth/AuthContext"; import { trackEvent } from "../../utils/analytics"; function isAuthed() { try { const auth = loadAuth(); return !!(auth && auth.access_token); } catch { return false; } } function requestAuth(message) { try { window.dispatchEvent( new CustomEvent("sociowire:auth-required", { detail: { message, mode: "login" }, }) ); } catch {} trackEvent("auth_required", { context: "wall", mode: "login" }); } // Time formatting is now handled in the component with translations function formatRelativeTime(createdAt) { try { if (!createdAt) return "now"; const d = createdAt instanceof Date ? createdAt : new Date(String(createdAt)); if (Number.isNaN(d.getTime())) return "now"; const s = Math.floor((Date.now() - d.getTime()) / 1000); if (s < 60) return "now"; const m = Math.floor(s / 60); if (m < 60) return `${m}m`; const h = Math.floor(m / 60); if (h < 48) return `${h}h`; const days = Math.floor(h / 24); return `${days}d`; } catch { return "now"; } } function formatCount(value) { const n = Number(value); if (!Number.isFinite(n)) return "0"; if (n < 1000) return String(Math.max(0, Math.floor(n))); if (n < 1000000) return `${(n / 1000).toFixed(1)}k`; return `${(n / 1000000).toFixed(1)}m`; } function categoryLabel(post) { const c = (post?.category || "NEWS").toString().toUpperCase(); if (c === "EVENT") return "EVENTS"; return c; } function categoryIconClass(post) { const c = (post?.category || "NEWS").toString().toUpperCase(); switch (c) { case "EVENT": case "EVENTS": return "fa-regular fa-calendar"; case "FRIENDS": return "fa-solid fa-user-group"; case "MARKET": return "fa-solid fa-store"; default: return "fa-regular fa-newspaper"; } } function normalizeImageUrl(raw) { const v = (raw || "").toString().trim(); if (!v) return ""; const lower = v.toLowerCase(); if ( lower === "null" || lower === "undefined" || lower === "none" || lower === "false" || lower === "0" || lower === "self" || lower === "default" ) { return ""; } if (lower.startsWith("data:")) return ""; if ( lower.startsWith("http://") || lower.startsWith("https://") || lower.startsWith("//") || lower.startsWith("/") || lower.startsWith("./") ) { return v; } return ""; } const wallAvatarCache = new Map(); const wallAvatarInflight = new Map(); async function getAvatarForUser(name) { const key = (name || "").toLowerCase().trim(); if (!key) return ""; if (wallAvatarCache.has(key)) return wallAvatarCache.get(key); if (wallAvatarInflight.has(key)) return wallAvatarInflight.get(key); const p = (async () => { const profile = await fetchProfile(name); const url = (profile && profile.avatar_url) ? String(profile.avatar_url) : ""; wallAvatarCache.set(key, url); wallAvatarInflight.delete(key); return url; })().catch(() => { wallAvatarCache.set(key, ""); wallAvatarInflight.delete(key); return ""; }); wallAvatarInflight.set(key, p); return p; } export default function PostCard({ post, selected, onSelect, onPostDeleted, onPostUpdated }) { const { t } = useTranslation(); const [liked, setLiked] = useState(false); const [authed, setAuthed] = useState(isAuthed()); const [activeImageIndex, setActiveImageIndex] = useState(0); const [failedImageIndices, setFailedImageIndices] = useState(() => new Set()); const [imgFailed, setImgFailed] = useState(false); const [showPrivacy, setShowPrivacy] = useState(false); const [privacySaving, setPrivacySaving] = useState(false); const [privacyError, setPrivacyError] = useState(""); const [postVisibility, setPostVisibility] = useState( (post?.visibility || "public").toString() ); const [postGroupId, setPostGroupId] = useState( (post?.group_id || "").toString() ); const [postUsers, setPostUsers] = useState(""); const [applyToImages, setApplyToImages] = useState(true); const [counts, setCounts] = useState(() => ({ like: post?.like_count ?? 0, comment: post?.comment_count ?? 0, share: post?.share_count ?? 0, })); const { username, token } = useAuth(); // Edit/Delete state const [showEditDelete, setShowEditDelete] = useState(false); const [isEditing, setIsEditing] = useState(false); const [editTitle, setEditTitle] = useState(""); const [editSnippet, setEditSnippet] = useState(""); const [editSaving, setEditSaving] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [deleting, setDeleting] = useState(false); const postId = post?.id ?? post?._id ?? 0; useEffect(() => { let active = true; if (!authed || !postId) return () => {}; fetchPostLikeState(postId).then((res) => { if (!active) return; if (res?.ok && res.liked) setLiked(true); }); return () => { active = false; }; }, [authed, postId]); useEffect(() => { const handler = () => { const next = isAuthed(); setAuthed(next); if (!next) setLiked(false); }; window.addEventListener("sociowire:auth-changed", handler); return () => window.removeEventListener("sociowire:auth-changed", handler); }, []); useEffect(() => { setCounts({ like: post?.like_count ?? 0, comment: post?.comment_count ?? 0, share: post?.share_count ?? 0, }); }, [post?.like_count, post?.comment_count, post?.share_count]); useEffect(() => { setPostVisibility((post?.visibility || "public").toString()); setPostGroupId((post?.group_id || "").toString()); }, [post?.visibility, post?.group_id]); const mediaUrls = useMemo(() => { const list = []; const seen = new Set(); const addUrl = (value) => { const normalized = normalizeImageUrl(value); if (!normalized || seen.has(normalized)) { return; } seen.add(normalized); list.push(normalized); }; if (Array.isArray(post?.media_urls)) { post.media_urls.forEach(addUrl); } [post?.image_large, post?.image_medium, post?.image_small, post?.image, post?.media_url].forEach(addUrl); return list; }, [post?.media_urls, post?.image_large, post?.image_medium, post?.image_small, post?.image, post?.media_url]); const mediaKey = mediaUrls.join("|"); useEffect(() => { setActiveImageIndex(0); setFailedImageIndices(new Set()); setImgFailed(false); }, [mediaKey]); const title = post?.title || "Untitled"; const snippet = post?.snippet || ""; const author = (post?.author || post?.Author || "anon").toString(); const isAuthor = username && author && author.toLowerCase() === username.toLowerCase(); const [authorAvatar, setAuthorAvatar] = useState(""); const sub = (post?.sub_category || post?.subCategory || "").toString(); const timeAgo = formatRelativeTime(post?.created_at || post?.CreatedAt || post?.createdAt); const cat = categoryLabel(post); const metaLine = useMemo(() => { const parts = []; if (sub) parts.push(sub); if (post?.region) parts.push(post.region); return parts.join(" • "); }, [sub, post?.region]); const safeImageIndex = mediaUrls.length ? Math.min(Math.max(activeImageIndex, 0), mediaUrls.length - 1) : 0; const activeImage = mediaUrls[safeImageIndex] || ""; const hasGalleryImage = Boolean(activeImage && !imgFailed); const findNextAvailableIndex = (current, failed) => { if (mediaUrls.length === 0) return -1; for (let offset = 1; offset <= mediaUrls.length; offset += 1) { const idx = (current + offset) % mediaUrls.length; if (!failed.has(idx)) { return idx; } } return -1; }; const findPrevAvailableIndex = (current, failed) => { if (mediaUrls.length === 0) return -1; for (let offset = 1; offset <= mediaUrls.length; offset += 1) { const idx = (current - offset + mediaUrls.length) % mediaUrls.length; if (!failed.has(idx)) { return idx; } } return -1; }; const handleImageError = () => { if (!activeImage) { setImgFailed(true); return; } const nextFailed = new Set(failedImageIndices); nextFailed.add(safeImageIndex); setFailedImageIndices(nextFailed); if (nextFailed.size >= mediaUrls.length) { setImgFailed(true); return; } const nextIndex = findNextAvailableIndex(safeImageIndex, nextFailed); if (nextIndex >= 0) { setActiveImageIndex(nextIndex); } }; useEffect(() => { let cancelled = false; const who = (author || "").trim(); if (!who) { setAuthorAvatar(""); return; } getAvatarForUser(who).then((url) => { if (cancelled) return; setAuthorAvatar(url || ""); }); return () => { cancelled = true; }; }, [author]); return (
{ trackEvent("post_view", { post_id: postId, post_title: title?.slice(0, 100), post_category: cat, post_author: author, content_type: "post", }); onSelect && onSelect(post); }} >
{authorAvatar ? {author} : (author ? author[0]?.toUpperCase() : "U")}
{author}
{timeAgo}
{cat}
{hasGalleryImage ? (
{mediaUrls.length > 1 && (
{safeImageIndex + 1}/{mediaUrls.length}
)} {mediaUrls.length > 1 && ( <> )}
{mediaUrls.length > 1 && (
{mediaUrls.map((value, idx) => ( ))}
)}
) : ( )}
{title}
{snippet ?
{snippet}
: null} {metaLine ?
{metaLine}
: null}
{formatCount(counts.like)} {formatCount(counts.comment)} {formatCount(counts.share)}
{isAuthor ? ( <> ) : null}
{isAuthor && showPrivacy ? (
e.stopPropagation()}>
{postVisibility === "group" ? ( setPostGroupId(e.target.value)} /> ) : null} {postVisibility === "custom" ? ( setPostUsers(e.target.value)} /> ) : null} {privacyError ?
{privacyError}
: null}
) : null} {isAuthor && showEditDelete ? (
e.stopPropagation()}> {!isEditing && !showDeleteConfirm ? ( <> ) : null} {isEditing ? (
setEditTitle(e.target.value)} />