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:
parent
a3e72c56ee
commit
5d5fae63c5
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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" });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue