diff --git a/src/api/client.js b/src/api/client.js index ca35a12..07c354e 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -246,6 +246,40 @@ export async function fetchPostById(postId) { } } +export async function fetchPostTruth(postId) { + if (!postId) return null; + const qs = new URLSearchParams(); + qs.set("post_id", String(postId)); + try { + const res = await fetch(`${API_BASE}/post/truth?${qs.toString()}`, { + credentials: "include", + headers: { ...authHeaders() }, + }); + if (!res.ok) return null; + const data = await res.json().catch(() => null); + return data?.truth || null; + } catch { + return null; + } +} + +export async function votePostTruth(postId, vote) { + if (!postId) throw new Error("post_id required"); + const body = JSON.stringify({ post_id: postId, vote }); + const res = await fetch(`${API_BASE}/post/truth-vote`, { + method: "POST", + headers: { "Content-Type": "application/json", ...authHeaders() }, + credentials: "include", + body, + }); + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(text || "truth vote failed"); + } + const data = await res.json().catch(() => ({})); + return data?.truth || null; +} + export async function updatePostVisibility(postId, payload = {}, token) { if (!postId) throw new Error("post_id required"); const headers = { "Content-Type": "application/json" }; diff --git a/src/components/Map/MapView.jsx b/src/components/Map/MapView.jsx index f659c09..3195c33 100644 --- a/src/components/Map/MapView.jsx +++ b/src/components/Map/MapView.jsx @@ -71,6 +71,8 @@ export default function MapView({ const overlayTimerRef = useRef(null); const pendingOpenPostIdRef = useRef(null); const pendingOpenFullRef = useRef(false); + const searchFitQueryRef = useRef(""); + const searchUserMovedRef = useRef(false); const openOverlayWhenReady = useCallback( (post, opts = {}) => { @@ -95,7 +97,7 @@ export default function MapView({ [mapRef, theme] ); - const { status, visiblePosts, loadingPosts, loadError, handleIncomingPost } = + const { status, visiblePosts, loadingPosts, loadError, handleIncomingPost, handlePostUpdate } = usePostsEngine({ theme, mapRef, @@ -312,12 +314,18 @@ export default function MapView({ ws.onmessage = (ev) => { try { const msg = JSON.parse(ev.data); - if (msg.type === "new_post" && msg.post) handleIncomingPost(msg.post); + if (msg.type === "new_post" && msg.post) { + handleIncomingPost(msg.post); + return; + } + if (msg.type === "post_update") { + handlePostUpdate(msg); + } } catch {} }; return () => ws && ws.close(); - }, [handleIncomingPost]); + }, [handleIncomingPost, handlePostUpdate]); const handleFlyToMe = () => { const map = mapRef.current; @@ -422,13 +430,15 @@ export default function MapView({ } }; - // Zoom intelligent: quand on cherche, zoom pour voir tous les résultats + // Zoom intelligent: quand on cherche, zoom pour voir tous les résultats (une seule fois) useEffect(() => { if (!searchQuery || !visiblePosts.length) return; if (skipAutoZoomRef.current) { skipAutoZoomRef.current = false; return; } + if (searchUserMovedRef.current) return; + if (searchFitQueryRef.current === searchQuery) return; const map = mapRef.current; if (!map) return; @@ -472,12 +482,29 @@ export default function MapView({ duration: 1000 } ); + searchFitQueryRef.current = searchQuery; } }, 300); // Délai pour laisser les markers se créer return () => clearTimeout(timer); }, [searchQuery, visiblePosts, mapRef]); + useEffect(() => { + searchFitQueryRef.current = ""; + searchUserMovedRef.current = false; + }, [searchQuery]); + + useEffect(() => { + const map = mapRef.current; + if (!map) return; + if (!searchQuery) return; + const handleMove = () => { + searchUserMovedRef.current = true; + }; + map.on("movestart", handleMove); + return () => map.off("movestart", handleMove); + }, [mapRef, searchQuery]); + const handleProfileVisitIsland = (island) => { console.log('[MapView] Visiting island from profile:', island.username); setSelectedProfile(null); // Close profile modal diff --git a/src/components/Map/markerManager.js b/src/components/Map/markerManager.js index c2526b0..8624e47 100644 --- a/src/components/Map/markerManager.js +++ b/src/components/Map/markerManager.js @@ -18,6 +18,8 @@ import { createPostComment, fetchPostLikeState, fetchProfile, + fetchPostTruth, + votePostTruth, } from "../../api/client"; import { trackEvent } from "../../utils/analytics"; @@ -126,6 +128,38 @@ function updateTruth(modalWrap, post) { if (label) label.textContent = `${score}`; } +function updateTruthVoteUI(modalWrap, stats) { + if (!modalWrap || !stats) return; + const aiEl = modalWrap.querySelector("[data-truth-ai]"); + const userEl = modalWrap.querySelector("[data-truth-user]"); + const countEl = modalWrap.querySelector("[data-truth-count]"); + const rail = modalWrap.querySelector(".sw-truth-rail"); + const marker = rail ? rail.querySelector(".sw-truth-rail-marker") : null; + const label = rail ? rail.querySelector(".sw-truth-rail-score") : null; + + const aiScore = Number.isFinite(stats.ai_score) ? Math.round(stats.ai_score) : stats.AIScore; + const userScore = Number.isFinite(stats.user_score) ? Math.round(stats.user_score) : stats.UserScore; + const truthScoreValue = Number.isFinite(stats.truth_score) ? Math.round(stats.truth_score) : stats.TruthScore; + const totalVotes = Number.isFinite(stats.total_votes) ? stats.total_votes : stats.TotalVotes; + + if (aiEl) aiEl.textContent = Number.isFinite(aiScore) ? String(aiScore) : "—"; + if (userEl) userEl.textContent = Number.isFinite(userScore) ? String(userScore) : "—"; + if (countEl) countEl.textContent = `${totalVotes || 0} votes`; + if (marker && Number.isFinite(truthScoreValue)) marker.style.top = `${100 - truthScoreValue}%`; + if (label && Number.isFinite(truthScoreValue)) label.textContent = `${truthScoreValue}`; + + const vote = (stats.user_vote || stats.UserVote || "").toString(); + const btns = modalWrap.querySelectorAll(".sw-truth-vote-btn"); + btns.forEach((btn) => { + const v = btn.getAttribute("data-truth-vote"); + btn.classList.toggle("is-active", vote && v === vote); + }); + const railUp = modalWrap.querySelector(".sw-truth-rail-btn-up"); + const railDown = modalWrap.querySelector(".sw-truth-rail-btn-down"); + if (railUp) railUp.classList.toggle("is-active", vote === "true"); + if (railDown) railDown.classList.toggle("is-active", vote === "false"); +} + function isAuthed() { try { const auth = loadAuth(); @@ -272,6 +306,34 @@ function mountCard(container, spec, data, theme, post) { ) ); + const rawScore = Number( + post?.truth_score ?? + post?.truthScore ?? + post?.truth_ai ?? + post?.truthAi ?? + post?.ai_score ?? + post?.AIScore + ); + const hasAI = Number.isFinite( + Number(post?.truth_ai ?? post?.truthAi ?? post?.ai_score ?? post?.AIScore) + ); + const hasScore = Number.isFinite(rawScore) && !(rawScore === 50 && !hasAI); + if (!hasScore && post?.id) { + fetchPostTruth(post.id).then((stats) => { + if (!stats) return; + const nextScore = Number(stats.truth_score ?? stats.TruthScore); + if (!Number.isFinite(nextScore)) return; + post.truth_score = nextScore; + const markerEl = container.querySelector(".sw-truth-mini-marker"); + const scoreEl = container.querySelector(".sw-truth-mini-score"); + if (markerEl) markerEl.style.top = `${100 - nextScore}%`; + if (scoreEl) { + scoreEl.style.background = truthColor(nextScore); + scoreEl.textContent = `${Math.round(nextScore)}`; + } + }); + } + return () => { try { root.unmount(); } catch {} }; @@ -353,16 +415,18 @@ function getPostTime(post) { } function truthScore(post) { - const raw = Number(post?.truth_score || post?.truthScore); + const raw = Number( + post?.truth_score ?? + post?.truthScore ?? + post?.truth_ai ?? + post?.truthAi ?? + post?.ai_score ?? + post?.AIScore + ); if (Number.isFinite(raw)) { return clamp(Math.round(raw), 0, 100); } - const seed = `${post?.id || ""}|${post?.url || ""}|${post?.title || ""}`; - let h = 0; - for (let i = 0; i < seed.length; i++) { - h = (h * 31 + seed.charCodeAt(i)) >>> 0; - } - return 35 + (h % 61); + return 50; } function truthColor(score) { @@ -414,6 +478,16 @@ function closeCenteredOverlay(map, opts = {}) { map.__swCenteredOverlay = null; + try { + const st = ov.interactionState || {}; + if (map) { + if (st.dragPan) map.dragPan?.enable?.(); + if (st.scrollZoom) map.scrollZoom?.enable?.(); + if (st.doubleClickZoom) map.doubleClickZoom?.enable?.(); + if (st.touchZoomRotate) map.touchZoomRotate?.enable?.(); + } + } catch {} + if (!skipHistory && ov.historyPushed && !ov.historyClosed) { ov.historyClosed = true; try { window.history.back(); } catch {} @@ -426,6 +500,17 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) { closeCenteredOverlay(map, { force: true, reason: "switch", skipHistory: true }); + const interactionState = { + dragPan: map.dragPan && map.dragPan.isEnabled ? map.dragPan.isEnabled() : null, + scrollZoom: map.scrollZoom && map.scrollZoom.isEnabled ? map.scrollZoom.isEnabled() : null, + doubleClickZoom: map.doubleClickZoom && map.doubleClickZoom.isEnabled ? map.doubleClickZoom.isEnabled() : null, + touchZoomRotate: map.touchZoomRotate && map.touchZoomRotate.isEnabled ? map.touchZoomRotate.isEnabled() : null, + }; + try { if (map.dragPan) map.dragPan.disable(); } catch {} + try { if (map.scrollZoom) map.scrollZoom.disable(); } catch {} + try { if (map.doubleClickZoom) map.doubleClickZoom.disable(); } catch {} + try { if (map.touchZoomRotate) map.touchZoomRotate.disable(); } catch {} + let postData = { ...(post || {}) }; const data = adaptPostToTemplateData(postData); const cat = normCatLabel(postData); @@ -494,12 +579,12 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {