From 8fa842ecb7364a6639ad82d97913972718e46795 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 25 Dec 2025 00:45:15 -0500 Subject: [PATCH] Refine sociowall feed and post media fallbacks --- src/App.jsx | 13 +- src/components/Map/markerManager.js | 44 ++++- src/components/Map/templateSpecs.js | 3 +- src/components/Map/usePostsEngine.js | 4 +- src/components/Posts/PostCard.jsx | 274 +++++++++++++++++++++++++-- src/components/Posts/PostList.jsx | 30 ++- src/styles/mapMarkers.css | 85 +++++++++ src/styles/posts.css | 154 +++++++++++++-- 8 files changed, 566 insertions(+), 41 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 0fe3982..43515cd 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -62,6 +62,17 @@ export default function App() { const [activeChats, setActiveChats] = useState([]); const [showContactsList, setShowContactsList] = useState(false); + useEffect(() => { + const onScroll = () => { + const threshold = window.innerHeight * 0.55; + const expanded = window.scrollY > threshold; + document.body.classList.toggle("sw-wall-expanded", expanded); + }; + window.addEventListener("scroll", onScroll, { passive: true }); + onScroll(); + return () => window.removeEventListener("scroll", onScroll); + }, []); + useEffect(() => { try { const saved = localStorage.getItem(ACTIVE_CHATS_KEY); @@ -187,4 +198,4 @@ export default function App() { )} ); -} \ No newline at end of file +} diff --git a/src/components/Map/markerManager.js b/src/components/Map/markerManager.js index ba8b759..6455bb9 100644 --- a/src/components/Map/markerManager.js +++ b/src/components/Map/markerManager.js @@ -87,7 +87,7 @@ function applyPostDetails(modalWrap, post) { const title = (post?.title || post?.Title || "Untitled").toString(); const summary = (post?.snippet || post?.body || "").toString(); - const img = post?.image_large || post?.image_small || post?.image || post?.media_url || ""; + const img = heroImageForPost(post); const url = (post?.url || "").toString().trim(); const titleEl = modalWrap.querySelector(".sw-modal-title"); @@ -100,7 +100,7 @@ function applyPostDetails(modalWrap, post) { if (hero) { hero.innerHTML = img ? `` - : `
`; + : `
${escapeHtml(normCatLabel(post))}
`; } const badge = modalWrap.querySelector(".sw-modal-badge"); @@ -151,7 +151,13 @@ function renderComments(container, items) { .map((c) => { const who = (c?.username || "anon").toString().trim(); const body = (c?.body || "").toString().trim(); - return `
${escapeHtml(who)}: ${escapeHtml(body)}
`; + const ts = (c?.created_at || "").toString().trim(); + return ` +
+
${escapeHtml(who)}${ts ? ` · ${escapeHtml(ts)}` : ""}
+
${escapeHtml(body)}
+
+ `; }) .join(""); } @@ -196,6 +202,34 @@ function normCatLabel(post) { return "NEWS"; } +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 heroImageForPost(post) { + const raw = + post?.image_large || + post?.image_small || + post?.image || + post?.media_url || + ""; + const v = (raw || "").toString().trim(); + if (v) return v; + const cat = normCatLabel(post); + return `/og/category?cat=${encodeURIComponent(cat)}`; +} + function formatRelativeTime(createdAt) { try { if (!createdAt) return "now"; @@ -257,7 +291,7 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) { const sub = (postData?.sub_category || postData?.subCategory || "").toString().trim(); const metaLeft = author ? `by ${author}` : ""; const metaRight = sub ? sub : ""; - const img = data?.image || ""; + const img = heroImageForPost(postData); const url = data?.url || ""; const postID = postData?.id || postData?.ID || 0; const viewCount = readCount(postData, ["view_count", "views", "viewCount"]); @@ -309,7 +343,7 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) { ${ img ? `` - : `
` + : `
${escapeHtml(normCatLabel(postData))}
` } diff --git a/src/components/Map/templateSpecs.js b/src/components/Map/templateSpecs.js index 38bf3c7..3285e7d 100644 --- a/src/components/Map/templateSpecs.js +++ b/src/components/Map/templateSpecs.js @@ -267,7 +267,8 @@ export function adaptPostToTemplateData(post) { const source = author || sub || ""; const summary = post?.snippet || post?.body || ""; const url = post?.url || ""; - const image = post?.image_large || post?.image_small || post?.image || post?.media_url || ""; + const rawImage = post?.image_large || post?.image_small || post?.image || post?.media_url || ""; + const image = rawImage || `/og/category?cat=${encodeURIComponent((post?.category || "NEWS").toString().toUpperCase())}&variant=mini`; const categoryIcon = getCategoryIcon(post); return { headline, source, summary, url, image, categoryIcon }; } diff --git a/src/components/Map/usePostsEngine.js b/src/components/Map/usePostsEngine.js index 143ff88..db77ead 100644 --- a/src/components/Map/usePostsEngine.js +++ b/src/components/Map/usePostsEngine.js @@ -720,7 +720,7 @@ export function usePostsEngine({ const exists = (allPostsRef.current || []).some((x) => getPostKey(x) === key); if (exists) return; } - allPostsRef.current = [...allPostsRef.current, p]; + allPostsRef.current = [p, ...allPostsRef.current]; const catCode = categoryCode(mainFilter); if (catCode) { @@ -743,7 +743,7 @@ export function usePostsEngine({ createMarkerForPost(p, mapRef, markersRef, expandedElRef, theme); } - setVisiblePosts((current) => [...current, p]); + setVisiblePosts((current) => [p, ...current]); }, [mainFilter, subFilter, timeFilter, searchQuery, mapRef, markersRef, expandedElRef, theme, refreshVisibleAndMarkers] ); diff --git a/src/components/Posts/PostCard.jsx b/src/components/Posts/PostCard.jsx index 96172e7..cd53770 100644 --- a/src/components/Posts/PostCard.jsx +++ b/src/components/Posts/PostCard.jsx @@ -1,27 +1,269 @@ -import React from "react"; -import CardRenderer from "../Cards/CardRenderer"; -import { cardTokens } from "../../theme/cardTokens"; -import { getTemplateSpecForPost, adaptPostToTemplateData } from "../Map/templateSpecs"; +import React, { useEffect, useMemo, useState } from "react"; +import { loadAuth } from "../../auth/authStorage"; +import { + fetchPostLikeState, + recordPostShare, + togglePostLike, +} from "../../api/client"; + +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 {} +} + +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 ""; +} export default function PostCard({ post, selected, onSelect }) { - const spec = getTemplateSpecForPost(post, "mini"); - const data = adaptPostToTemplateData(post); + const [liked, setLiked] = useState(false); + const [authed, setAuthed] = useState(isAuthed()); + const [imgFailed, setImgFailed] = useState(false); + const [counts, setCounts] = useState(() => ({ + like: post?.like_count ?? 0, + comment: post?.comment_count ?? 0, + share: post?.share_count ?? 0, + })); + + 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]); + + const rawImg = post?.image_large || post?.image_small || post?.image || post?.media_url || ""; + const img = normalizeImageUrl(rawImg); + const title = post?.title || "Untitled"; + const snippet = post?.snippet || ""; + const author = (post?.author || post?.Author || "anon").toString(); + 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]); return (
onSelect && onSelect(post)} - style={{ padding: 6 }} > -
- { - if (action?.type === "open_url" && value) window.open(value, "_blank"); +
+
{author ? author[0]?.toUpperCase() : "U"}
+
+
{author}
+
{timeAgo}
+
+
{cat}
+
+ + {img && !imgFailed ? ( +
+ setImgFailed(true)} + /> +
+ ) : ( + + )} + +
{title}
+ {snippet ?
{snippet}
: null} + {metaLine ?
{metaLine}
: null} + +
+ {formatCount(counts.like)} + {formatCount(counts.comment)} + {formatCount(counts.share)} +
+ +
+ + +
); diff --git a/src/components/Posts/PostList.jsx b/src/components/Posts/PostList.jsx index ee5e569..759d540 100644 --- a/src/components/Posts/PostList.jsx +++ b/src/components/Posts/PostList.jsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import PostCard from "./PostCard"; function getKmFromPost(post) { @@ -7,6 +7,9 @@ function getKmFromPost(post) { export default function PostList({ posts, loading, error, selectedPost, onSelectPost }) { const [kmFilter, setKmFilter] = useState(1000); + const [visibleCount, setVisibleCount] = useState(6); + const listRef = useRef(null); + const sentinelRef = useRef(null); const filtered = useMemo( () => @@ -18,8 +21,28 @@ export default function PostList({ posts, loading, error, selectedPost, onSelect [posts, kmFilter] ); + useEffect(() => { + setVisibleCount(6); + }, [kmFilter]); + + useEffect(() => { + const root = listRef.current || null; + const sentinel = sentinelRef.current; + if (!sentinel) return; + const observer = new IntersectionObserver( + (entries) => { + const hit = entries && entries[0]; + if (!hit || !hit.isIntersecting) return; + setVisibleCount((c) => Math.min(filtered.length, c + 6)); + }, + { root, rootMargin: "200px 0px", threshold: 0.01 } + ); + observer.observe(sentinel); + return () => observer.disconnect(); + }, [filtered.length]); + return ( -
+