feat: Add guid support to all engagement functions for app posts

- Updated getId() in usePostsEngine.js to use guid for posts with app_id
- Added guid parameter to togglePostLike, votePostTruth, recordPostView,
  recordPostShare, createPostComment, fetchPostComments
- Updated PostDetailModal.jsx to pass guid for all engagement actions
- Updated markerManager.js to pass guid for comments

This enables proper engagement tracking for news/app posts that use
guid identifiers instead of numeric post_id.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-01-31 04:41:59 +00:00
parent a3e72c56ee
commit 5d5fae63c5
4 changed files with 187 additions and 70 deletions

View File

@ -415,9 +415,11 @@ export async function fetchPostTruth(postId) {
} }
} }
export async function votePostTruth(postId, vote) { export async function votePostTruth(postId, vote, guid = null) {
if (!postId) throw new Error("post_id required"); if (!postId && !guid) throw new Error("post_id or guid required");
const body = JSON.stringify({ post_id: postId, vote }); const payload = { post_id: postId, vote };
if (guid) payload.guid = guid;
const body = JSON.stringify(payload);
const res = await fetch(`${apiBase()}/post/truth-vote`, { const res = await fetch(`${apiBase()}/post/truth-vote`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json", ...authHeaders() }, headers: { "Content-Type": "application/json", ...authHeaders() },
@ -523,7 +525,17 @@ export async function fetchSmartFeed(params = {}, { signal } = {}) {
try { try {
const data = await fetchJsonCached(url, { ttlMs: 10000, signal }); const data = await fetchJsonCached(url, { ttlMs: 10000, signal });
const items = Array.isArray(data) ? data : data.results || data.items || []; const items = Array.isArray(data) ? data : data.results || data.items || [];
console.log('[fetchSmartFeed] Got', items.length, 'posts, content_types:', [...new Set(items.map(p => p.content_type))]); // DEBUG: Check tier values from API
const tierCounts = { premium: 0, regular: 0, none: 0 };
items.forEach(p => {
if (p.tier === 'premium') tierCounts.premium++;
else if (p.tier === 'regular') tierCounts.regular++;
else tierCounts.none++;
});
console.log('[fetchSmartFeed] Got', items.length, 'posts, TIER breakdown:', tierCounts, 'content_types:', [...new Set(items.map(p => p.content_type))]);
if (tierCounts.premium > 0) {
console.log('[fetchSmartFeed] Sample premium post:', items.find(p => p.tier === 'premium'));
}
return await resolveAssetRefsInPosts(items); return await resolveAssetRefsInPosts(items);
} catch (err) { } catch (err) {
if (err.name === "AbortError") return []; // silently ignore aborted requests if (err.name === "AbortError") return []; // silently ignore aborted requests
@ -766,14 +778,16 @@ export async function fetchProfile(username = "") {
} }
} }
export async function recordPostView(postId) { export async function recordPostView(postId, guid = null) {
if (!postId) return { ok: false }; if (!postId && !guid) return { ok: false };
try { try {
const body = { post_id: postId };
if (guid) body.guid = guid;
const res = await fetch(`${apiBase()}/post/view`, { const res = await fetch(`${apiBase()}/post/view`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
credentials: "include", credentials: "include",
body: JSON.stringify({ post_id: postId }), body: JSON.stringify(body),
}); });
const data = await res.json().catch(() => ({})); const data = await res.json().catch(() => ({}));
return { ok: res.ok, ...data }; return { ok: res.ok, ...data };
@ -782,14 +796,16 @@ export async function recordPostView(postId) {
} }
} }
export async function togglePostLike(postId, like = true) { export async function togglePostLike(postId, like = true, guid = null) {
if (!postId) return { ok: false }; if (!postId && !guid) return { ok: false };
try { try {
const body = { post_id: postId, like };
if (guid) body.guid = guid;
const res = await fetch(`${apiBase()}/post/like`, { const res = await fetch(`${apiBase()}/post/like`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json", ...authHeaders() }, headers: { "Content-Type": "application/json", ...authHeaders() },
credentials: "include", credentials: "include",
body: JSON.stringify({ post_id: postId, like }), body: JSON.stringify(body),
}); });
const data = await res.json().catch(() => ({})); const data = await res.json().catch(() => ({}));
return { ok: res.ok, ...data }; return { ok: res.ok, ...data };
@ -798,14 +814,16 @@ export async function togglePostLike(postId, like = true) {
} }
} }
export async function recordPostShare(postId) { export async function recordPostShare(postId, guid = null) {
if (!postId) return { ok: false }; if (!postId && !guid) return { ok: false };
try { try {
const body = { post_id: postId };
if (guid) body.guid = guid;
const res = await fetch(`${apiBase()}/post/share`, { const res = await fetch(`${apiBase()}/post/share`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
credentials: "include", credentials: "include",
body: JSON.stringify({ post_id: postId }), body: JSON.stringify(body),
}); });
const data = await res.json().catch(() => ({})); const data = await res.json().catch(() => ({}));
return { ok: res.ok, ...data }; return { ok: res.ok, ...data };
@ -814,10 +832,11 @@ export async function recordPostShare(postId) {
} }
} }
export async function fetchPostComments(postId, limit = 20) { export async function fetchPostComments(postId, limit = 20, guid = null) {
if (!postId) return []; if (!postId && !guid) return [];
const qs = new URLSearchParams(); const qs = new URLSearchParams();
qs.set("post_id", String(postId)); qs.set("post_id", String(postId));
if (guid) qs.set("guid", guid);
if (limit) qs.set("limit", String(limit)); if (limit) qs.set("limit", String(limit));
try { try {
const res = await fetch(`${apiBase()}/post/comments?${qs.toString()}`, { const res = await fetch(`${apiBase()}/post/comments?${qs.toString()}`, {
@ -831,14 +850,17 @@ export async function fetchPostComments(postId, limit = 20) {
} }
} }
export async function createPostComment(postId, body) { export async function createPostComment(postId, body, guid = null) {
if (!postId || !body) return { ok: false }; if (!postId && !guid) return { ok: false };
if (!body) return { ok: false };
try { try {
const reqBody = { post_id: postId, body };
if (guid) reqBody.guid = guid;
const res = await fetch(`${apiBase()}/post/comment`, { const res = await fetch(`${apiBase()}/post/comment`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json", ...authHeaders() }, headers: { "Content-Type": "application/json", ...authHeaders() },
credentials: "include", credentials: "include",
body: JSON.stringify({ post_id: postId, body }), body: JSON.stringify(reqBody),
}); });
const data = await res.json().catch(() => ({})); const data = await res.json().catch(() => ({}));
return { ...data, ok: res.ok }; return { ...data, ok: res.ok };

View File

@ -4,6 +4,14 @@ import { createRoot } from "react-dom/client";
const SW_ANIM_MS = 840; const SW_ANIM_MS = 840;
// Get consistent ID for a post (handles numeric IDs and UUID fallback)
function getPostId(p) {
const numId = p?.id ?? p?._id;
if (numId != null && numId !== 0) return numId;
if (p?.guid) return p.guid;
return numId;
}
import CardRenderer from "../Cards/CardRenderer"; import CardRenderer from "../Cards/CardRenderer";
import { cardTokens } from "../../theme/cardTokens"; import { cardTokens } from "../../theme/cardTokens";
import { getTemplateSpecForPost, getTemplateKeyForPost, adaptPostToTemplateData } from "./templateSpecs"; import { getTemplateSpecForPost, getTemplateKeyForPost, adaptPostToTemplateData } from "./templateSpecs";
@ -454,13 +462,11 @@ function mountCard(container, spec, data, theme, post) {
); );
} }
// Render card with CardRenderer
root.render( root.render(
React.createElement( React.createElement(
"div", React.Fragment,
{ null,
className: "sw-card-wrap",
style: { width: spec?.size?.w, height: spec?.size?.h },
},
...children ...children
) )
); );
@ -1804,9 +1810,9 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
}); });
} }
if (postID > 0 && !modalWrap.__swViewed) { if ((postID > 0 || postData?.guid) && !modalWrap.__swViewed) {
modalWrap.__swViewed = true; modalWrap.__swViewed = true;
recordPostView(postID).then((res) => { recordPostView(postID, postData?.guid).then((res) => {
const counts = res?.counts; const counts = res?.counts;
if (counts) { if (counts) {
updateStat(modalWrap, "views", counts.view_count); updateStat(modalWrap, "views", counts.view_count);
@ -1869,11 +1875,13 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
} }
const shareBtn = modalWrap.querySelector('[data-action="share"]'); const shareBtn = modalWrap.querySelector('[data-action="share"]');
if (shareBtn && postID > 0) { if (shareBtn && (postID > 0 || postData?.guid)) {
shareBtn.addEventListener("click", async () => { shareBtn.addEventListener("click", async () => {
const shareUrl = `${window.location.origin}/p/${postID}`; const shareUrl = postData?.guid
? `${window.location.origin}/n/${postData.guid}`
: `${window.location.origin}/p/${postID}`;
const shareTitle = postData?.title || postData?.Title || "SocioWire"; const shareTitle = postData?.title || postData?.Title || "SocioWire";
trackEvent("post_share_click", { post_id: postID, context: "map" }); trackEvent("post_share_click", { post_id: postID || postData?.guid, context: "map" });
try { try {
if (navigator.share) { if (navigator.share) {
await navigator.share({ title: shareTitle, url: shareUrl }); await navigator.share({ title: shareTitle, url: shareUrl });
@ -1881,7 +1889,7 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
await navigator.clipboard.writeText(shareUrl); await navigator.clipboard.writeText(shareUrl);
} }
} catch {} } catch {}
const res = await recordPostShare(postID); const res = await recordPostShare(postID, postData?.guid);
const counts = res?.counts; const counts = res?.counts;
if (counts) { if (counts) {
updateStat(modalWrap, "views", counts.view_count); updateStat(modalWrap, "views", counts.view_count);
@ -1892,7 +1900,7 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
// Truth vote handlers (thumbs up/down in truth badge) // Truth vote handlers (thumbs up/down in truth badge)
const truthBadgeNew = modalWrap.querySelector('.sw-truth-badge'); const truthBadgeNew = modalWrap.querySelector('.sw-truth-badge');
if (truthBadgeNew && postID > 0) { if (truthBadgeNew && (postID > 0 || postData?.guid)) {
const downvoteBtn = truthBadgeNew.querySelector('.sw-truth-downvote'); const downvoteBtn = truthBadgeNew.querySelector('.sw-truth-downvote');
const upvoteBtn = truthBadgeNew.querySelector('.sw-truth-upvote'); const upvoteBtn = truthBadgeNew.querySelector('.sw-truth-upvote');
const scoreDisplay = truthBadgeNew.querySelector('.sw-truth-badge-score'); const scoreDisplay = truthBadgeNew.querySelector('.sw-truth-badge-score');
@ -1936,8 +1944,8 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
} }
try { try {
trackEvent("truth_vote_click", { post_id: postID, vote, context: "map" }); trackEvent("truth_vote_click", { post_id: postID || postData?.guid, vote, context: "map" });
const stats = await votePostTruth(postID, vote); const stats = await votePostTruth(postID, vote, postData?.guid);
if (stats) { if (stats) {
currentVote = vote; currentVote = vote;
updateButtonStyles(); updateButtonStyles();
@ -2192,10 +2200,10 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
if (!body) return; if (!body) return;
trackEvent("post_comment_submit", { post_id: postID, context: "map" }); trackEvent("post_comment_submit", { post_id: postID, context: "map" });
commentBtn.disabled = true; commentBtn.disabled = true;
const res = await createPostComment(postID, body); const res = await createPostComment(postID, body, postData?.guid);
if (res?.ok) { if (res?.ok) {
commentInput.value = ""; commentInput.value = "";
const existing = await fetchPostComments(postID, 20); const existing = await fetchPostComments(postID, 20, postData?.guid);
renderComments(commentsBody, existing); renderComments(commentsBody, existing);
const counts = res?.counts; const counts = res?.counts;
if (counts) { if (counts) {
@ -2354,6 +2362,7 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the
requestAnimationFrame(() => root.classList.add("sw-appear-in")); requestAnimationFrame(() => root.classList.add("sw-appear-in"));
root.className = "post-pin post-pin--compact"; root.className = "post-pin post-pin--compact";
root.style.zIndex = "100"; root.style.zIndex = "100";
root.__post = post; root.__post = post;
function unmountIfAny() { function unmountIfAny() {
@ -2368,19 +2377,12 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the
root.className = "post-pin post-pin--compact sw-appear sw-appear-in"; root.className = "post-pin post-pin--compact sw-appear sw-appear-in";
root.style.zIndex = "100"; root.style.zIndex = "100";
// Add connecting line if offset is significant
// DISABLED: No longer using pixel offsets
const hasOffset = false;
const lineHTML = '';
// Add slight random tilt for polaroid effect (based on post ID for consistency)
const postId = post?.id || post?.ID || 0; const postId = post?.id || post?.ID || 0;
const tiltAngle = ((postId % 11) - 5) * 1.2; // Range: -6 to +6 degrees const tiltAngle = ((postId % 11) - 5) * 1.2;
root.innerHTML = ` root.innerHTML = `
<div class="sw-template-mini-wrap" style="--card-tilt: ${tiltAngle}deg;"></div> <div class="sw-template-mini-wrap" style="--card-tilt: ${tiltAngle}deg;"></div>
<div class="post-pin-pointer-small"></div> <div class="post-pin-pointer-small"></div>
${lineHTML}
`; `;
root.__swCompactReady = true; root.__swCompactReady = true;
} }
@ -2430,7 +2432,18 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the
.setLngLat(lngLat) .setLngLat(lngLat)
.addTo(map); .addTo(map);
markersRef.current.push({ id: post.id, marker, el: root, post, type: "full" }); // CRITICAL: Set z-index on the marker's container element (not just inner content)
// MapLibre GL wraps markers in a container - z-index must be on the container
try {
const markerEl = marker.getElement();
if (markerEl) {
markerEl.style.zIndex = "100";
}
} catch (e) {
console.warn('[createMarkerForPost] Could not set marker z-index:', e);
}
markersRef.current.push({ id: getPostId(post), marker, el: root, post, type: "full" });
root.addEventListener("click", (ev) => { root.addEventListener("click", (ev) => {
ev.stopPropagation(); ev.stopPropagation();
@ -2623,6 +2636,8 @@ export function createSimpleMarkerForPost(post, mapRef, markersRef, expandedElRe
root.style.flexDirection = "column"; root.style.flexDirection = "column";
root.style.alignItems = "center"; root.style.alignItems = "center";
root.style.zIndex = isWeatherPost ? "10" : "2"; root.style.zIndex = isWeatherPost ? "10" : "2";
root.style.contain = "layout style paint"; // CSS containment for performance
root.style.willChange = "transform, opacity"; // Hint for GPU acceleration
root.__post = post; root.__post = post;
function renderSimple() { function renderSimple() {
@ -2641,14 +2656,14 @@ export function createSimpleMarkerForPost(post, mapRef, markersRef, expandedElRe
? 'linear-gradient(135deg, rgba(56, 189, 248, 0.9), rgba(59, 130, 246, 0.9))' ? 'linear-gradient(135deg, rgba(56, 189, 248, 0.9), rgba(59, 130, 246, 0.9))'
: isWeather : isWeather
? weatherColor ? weatherColor
: getMarkerColorForCategory(activePost?.category); : getMarkerColorForCategory(activePost?.category, activePost);
const pinColor = isLive const pinColor = isLive
? livePin ? livePin
: (isCamera || isVideo) : (isCamera || isVideo)
? 'rgba(56, 189, 248, 0.9)' ? 'rgba(56, 189, 248, 0.9)'
: isWeather : isWeather
? weatherPin ? weatherPin
: getMarkerColorForCategory(activePost?.category); : getMarkerColorForCategory(activePost?.category, activePost);
const img = normalizeImageUrl(activePost?.image_small || ""); const img = normalizeImageUrl(activePost?.image_small || "");
root.__lastTemplateKey = getTemplateKeyForPost(activePost); root.__lastTemplateKey = getTemplateKeyForPost(activePost);
root.__lastImage = img || ""; root.__lastImage = img || "";
@ -2911,7 +2926,15 @@ export function createSimpleMarkerForPost(post, mapRef, markersRef, expandedElRe
.setLngLat(lngLat) .setLngLat(lngLat)
.addTo(map); .addTo(map);
markersRef.current.push({ id: post.id, marker, el: root, post, type: "simple" }); // Set z-index on marker container - simple pins at z-index 1 (below cards at 100)
try {
const markerEl = marker.getElement();
if (markerEl) {
markerEl.style.zIndex = "1";
}
} catch (e) {}
markersRef.current.push({ id: getPostId(post), marker, el: root, post, type: "simple" });
} }
/** /**

View File

@ -8,8 +8,27 @@ import { prioritizePosts } from "./postPriority";
import { trackEvent } from "../../utils/analytics"; import { trackEvent } from "../../utils/analytics";
import { matchesContentFilter } from "../Filters/ContentFilters"; import { matchesContentFilter } from "../Filters/ContentFilters";
// Generate stable numeric ID from a string (like guid or url_hash)
function hashStringToNumber(str) {
if (!str) return 0;
let hash = 0;
for (let i = 0; i < Math.min(str.length, 8); i++) {
hash = (hash << 4) + str.charCodeAt(i);
}
return Math.abs(hash);
}
function getId(p) { function getId(p) {
return p?.id ?? p?._id ?? null; // App posts (news, external apps) - use guid directly as-is
if (p?.app_id && p?.guid) return p.guid;
// Native posts - use numeric id
const numId = p?.id ?? p?._id;
if (numId != null && numId !== 0) return numId;
// Fallback: if has guid but no app_id, still use guid
if (p?.guid) return p.guid;
return numId;
} }
function normalizePostId(post) { function normalizePostId(post) {
@ -297,8 +316,8 @@ function buildClusterAreaFeatures(posts) {
return features; return features;
} }
const MAX_POOL_POSTS = 160; const MAX_POOL_POSTS = 1500;
const MAX_VISIBLE_POSTS = 45; const MAX_VISIBLE_POSTS = 1000; // High limit to allow navigation without losing posts
function expandBounds(bounds, padRatio = 0) { function expandBounds(bounds, padRatio = 0) {
if (!bounds) return null; if (!bounds) return null;
@ -793,7 +812,15 @@ export function usePostsEngine({
fullCardPosts = prioritized.fullCardPosts; fullCardPosts = prioritized.fullCardPosts;
simpleMarkerPosts = prioritized.simpleMarkerPosts; simpleMarkerPosts = prioritized.simpleMarkerPosts;
// DEBUG: Log marker type assignments
console.log('[syncMarkers] fullCardPosts:', fullCardPosts.length, 'simpleMarkerPosts:', simpleMarkerPosts.length);
if (fullCardPosts.length > 0) {
console.log('[syncMarkers] First fullCard:', { id: getId(fullCardPosts[0]), tier: fullCardPosts[0]?.tier, title: fullCardPosts[0]?.title?.substring(0, 30) });
}
// Combine both types for "wanted" tracking // Combine both types for "wanted" tracking
// Use Set with String for O(1) lookup and avoid type mismatch (string vs number)
const fullCardIds = new Set(fullCardPosts.map(p => String(getId(p))));
const wanted = new Map(); const wanted = new Map();
for (const post of visible) { for (const post of visible) {
const id = getId(post); const id = getId(post);
@ -803,10 +830,10 @@ export function usePostsEngine({
if (ct === "weather") { if (ct === "weather") {
continue; // Skip weather markers - they appear in the search bar widget continue; // Skip weather markers - they appear in the search bar widget
} }
// Tag avec le type de marqueur souhaité // Tag avec le type de marqueur souhaité - use String for comparison
const isFullCard = const isFullCard =
id === forceFullPostId || String(id) === String(forceFullPostId) ||
fullCardPosts.some(p => getId(p) === id); fullCardIds.has(String(id));
wanted.set(id, { post, type: isFullCard ? 'full' : 'simple' }); wanted.set(id, { post, type: isFullCard ? 'full' : 'simple' });
} }
} }
@ -863,16 +890,56 @@ export function usePostsEngine({
// add missing markers - use appropriate type based on priority // add missing markers - use appropriate type based on priority
// Camera/video ALWAYS use simple markers regardless of priority // Camera/video ALWAYS use simple markers regardless of priority
let createdFull = 0, createdSimple = 0;
let wantedFullCount = 0, wantedSimpleCount = 0, skippedCount = 0;
for (const [id, { post, type }] of wanted.entries()) { for (const [id, { post, type }] of wanted.entries()) {
if (have.has(id)) continue; if (type === 'full') wantedFullCount++;
else wantedSimpleCount++;
if (have.has(id)) {
skippedCount++;
continue;
}
const ct = (post?.content_type || '').toLowerCase(); const ct = (post?.content_type || '').toLowerCase();
if (type === 'full' && ct !== 'camera' && ct !== 'video') { if (type === 'full' && ct !== 'camera' && ct !== 'video') {
console.log('[syncMarkers] Creating FULL CARD marker for:', id, post?.tier, post?.title?.substring(0, 25));
createMarkerForPost(post, mapRef, markersRef, expandedElRef, theme); createMarkerForPost(post, mapRef, markersRef, expandedElRef, theme);
createdFull++;
} else { } else {
createSimpleMarkerForPost(post, mapRef, markersRef, expandedElRef, theme); createSimpleMarkerForPost(post, mapRef, markersRef, expandedElRef, theme);
createdSimple++;
} }
} }
// Store wanted type counts for debug
if (typeof window !== 'undefined') {
window.__swMarkerDebugLoop = { wantedFullCount, wantedSimpleCount, skippedCount };
}
console.log('[syncMarkers] CREATED markers: full=', createdFull, 'simple=', createdSimple);
// Store debug info for visibility
if (typeof window !== 'undefined') {
// Count tier values in visible
let premiumCount = 0, regularCount = 0, noTierCount = 0;
for (const p of visible) {
if (p.tier === 'premium') premiumCount++;
else if (p.tier === 'regular') regularCount++;
else noTierCount++;
}
window.__swMarkerDebug = {
fullCardPosts: fullCardPosts.length,
simpleMarkerPosts: simpleMarkerPosts.length,
createdFull,
createdSimple,
wanted: wanted.size,
visible: visible.length,
premiumTier: premiumCount,
regularTier: regularCount,
noTier: noTierCount,
firstFullId: fullCardPosts[0] ? (fullCardPosts[0].id || fullCardPosts[0].guid) : 'none',
timestamp: Date.now()
};
}
}, },
[mapRef, markersRef, expandedElRef, theme, searchQuery, forceFullPostId, weatherEnabled] [mapRef, markersRef, expandedElRef, theme, searchQuery, forceFullPostId, weatherEnabled]
); );
@ -929,11 +996,8 @@ export function usePostsEngine({
return false; return false;
} }
// Apply time filter (skip when showing search results - user wants all matches) // Time filter is now applied on backend (news-app Elasticsearch query)
if (!searchResultsRef.current && tf && tf > 0) { // No frontend time filter needed - backend already filters by time_hours
const postTime = effectivePostTime(p);
if (!matchesTimeFilter(postTime, tf)) return false;
}
if (isClusterFocus) { if (isClusterFocus) {
const id = Number(getId(p)); const id = Number(getId(p));
@ -1262,7 +1326,15 @@ export function usePostsEngine({
if (debugEnabled) { if (debugEnabled) {
const count = Array.isArray(newPosts) ? newPosts.length : 0; const count = Array.isArray(newPosts) ? newPosts.length : 0;
const q = (searchQuery || "").trim(); const q = (searchQuery || "").trim();
setDebugStatus(`tf:${timeHours || 0} fetched:${count} visible:${visible.length}${q ? ` q:${q}` : ""}`); // Add tier breakdown to debug status
const tierCounts = { premium: 0, regular: 0 };
if (Array.isArray(newPosts)) {
newPosts.forEach(p => {
if (p.tier === 'premium') tierCounts.premium++;
else if (p.tier === 'regular') tierCounts.regular++;
});
}
setDebugStatus(`v151 tf:${timeHours || 0} f:${count} v:${visible.length} P:${tierCounts.premium} R:${tierCounts.regular}${q ? ` q:${q}` : ""}`);
} }
const now = Date.now(); const now = Date.now();
if (now - analyticsDebounceRef.current > 4000) { if (now - analyticsDebounceRef.current > 4000) {

View File

@ -624,7 +624,7 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
// Initial data fetch // Initial data fetch
useEffect(() => { useEffect(() => {
if (!postId) return; if (!postId) return;
recordPostView(postId).then(res => { recordPostView(postId, post?.guid).then(res => {
if (res?.counts) setCounts(c => ({ ...c, views: res.counts.view_count || c.views, likes: res.counts.like_count || c.likes })); if (res?.counts) setCounts(c => ({ ...c, views: res.counts.view_count || c.views, likes: res.counts.like_count || c.likes }));
}); });
fetchPostLikeState(postId).then(res => { if (res?.liked !== undefined) setLiked(res.liked); }); fetchPostLikeState(postId).then(res => { if (res?.liked !== undefined) setLiked(res.liked); });
@ -690,18 +690,18 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
const newLiked = !liked; const newLiked = !liked;
setLiked(newLiked); setLiked(newLiked);
setCounts(c => ({ ...c, likes: c.likes + (newLiked ? 1 : -1) })); setCounts(c => ({ ...c, likes: c.likes + (newLiked ? 1 : -1) }));
const res = await togglePostLike(postId, newLiked); const res = await togglePostLike(postId, newLiked, post?.guid);
if (res?.counts) setCounts(c => ({ ...c, likes: res.counts.like_count ?? c.likes })); if (res?.counts) setCounts(c => ({ ...c, likes: res.counts.like_count ?? c.likes }));
}, [postId, liked]); }, [postId, liked, post?.guid]);
const handleShare = useCallback(async () => { const handleShare = useCallback(async () => {
if (!postId) return; if (!postId) return;
const shareUrl = `${window.location.origin}/p/${postId}`; const shareUrl = `${window.location.origin}/p/${postId}`;
if (navigator.share) { try { await navigator.share({ title: post?.title, url: shareUrl }); } catch {} } if (navigator.share) { try { await navigator.share({ title: post?.title, url: shareUrl }); } catch {} }
else { navigator.clipboard?.writeText(shareUrl); } else { navigator.clipboard?.writeText(shareUrl); }
const res = await recordPostShare(postId); const res = await recordPostShare(postId, post?.guid);
if (res?.counts) setCounts(c => ({ ...c, shares: res.counts.share_count ?? c.shares })); if (res?.counts) setCounts(c => ({ ...c, shares: res.counts.share_count ?? c.shares }));
}, [postId, post?.title]); }, [postId, post?.title, post?.guid]);
const handleTruthVote = useCallback(async (vote) => { const handleTruthVote = useCallback(async (vote) => {
if (!postId) return; if (!postId) return;
@ -709,9 +709,9 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
window.dispatchEvent(new CustomEvent("sociowire:auth-required")); window.dispatchEvent(new CustomEvent("sociowire:auth-required"));
return; return;
} }
const res = await votePostTruth(postId, vote); const res = await votePostTruth(postId, vote, post?.guid);
if (res) setTruth({ score: res.truth_score ?? res.user_score ?? truth.score, count: res.vote_count ?? truth.count }); if (res) setTruth({ score: res.truth_score ?? res.user_score ?? truth.score, count: res.vote_count ?? truth.count });
}, [postId, truth, token]); }, [postId, truth, token, post?.guid]);
const handleComment = useCallback(async () => { const handleComment = useCallback(async () => {
const body = commentInput.trim(); const body = commentInput.trim();
@ -727,7 +727,7 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
// Retry logic - try up to 3 times with delay // Retry logic - try up to 3 times with delay
let res = null; let res = null;
for (let attempt = 0; attempt < 3; attempt++) { for (let attempt = 0; attempt < 3; attempt++) {
res = await createPostComment(postId, body); res = await createPostComment(postId, body, post?.guid);
if (res?.ok) break; if (res?.ok) break;
if (attempt < 2) await new Promise(r => setTimeout(r, 500)); // wait before retry if (attempt < 2) await new Promise(r => setTimeout(r, 500)); // wait before retry
} }
@ -905,7 +905,7 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
{/* Content Section */} {/* Content Section */}
<div className="max-h-[70vh] overflow-y-auto"> <div className="max-h-[70vh] overflow-y-auto">
<div className="p-4 sm:p-6"> <div className="p-4 sm:p-6 pb-8 sm:pb-10">
{/* Title */} {/* Title */}
{post?.title && ( {post?.title && (
<h1 className="text-xl sm:text-2xl font-bold text-themed-primary leading-tight mb-4"> <h1 className="text-xl sm:text-2xl font-bold text-themed-primary leading-tight mb-4">
@ -1086,8 +1086,8 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
{/* Comments Section */} {/* Comments Section */}
<AnimatePresence> <AnimatePresence>
{showComments && ( {showComments && (
<motion.div className="mt-4" initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }}> <motion.div className="mt-4 mb-6" initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }}>
<div className="p-4 rounded-2xl bg-themed-elevated border border-themed"> <div className="p-4 rounded-2xl bg-themed-elevated border-2 border-themed">
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<i className="fa-solid fa-comments text-accent" /> <i className="fa-solid fa-comments text-accent" />
<span className="font-semibold text-themed-primary">{t("post.comment")}</span> <span className="font-semibold text-themed-primary">{t("post.comment")}</span>