import { useCallback, useEffect, useRef, useState } from "react"; import { fetchPosts } from "../../api/client"; import { categoryCode, matchesSubFilter, matchesTimeFilter } from "./mapFilter"; import { haversineKm } from "./mapGeo"; import { createMarkerForPost, clearAllMarkers } from "./markerManager"; export function usePostsEngine({ mapRef, viewParams, mainFilter, subFilter, timeFilter, markersRef, expandedElRef, }) { const allPostsRef = useRef([]); const lastFetchRef = useRef({ center: null, radiusKm: null, filterKey: "" }); const delayedFetchRef = useRef(null); const [status, setStatus] = useState("Loading posts..."); const [visiblePosts, setVisiblePosts] = useState([]); const [loadingPosts, setLoadingPosts] = useState(false); const [loadError, setLoadError] = useState(""); const rebuildMarkersForFilters = useCallback( (tf) => { const map = mapRef.current; if (!map) return; // If expanded is open and we are inside ignore window, do not rebuild (it closes the popup) try { const until = map.__swIgnoreRebuildUntil || 0; if (expandedElRef?.current && until && Date.now() < until) return; } catch {} clearAllMarkers(markersRef, expandedElRef); const posts = allPostsRef.current || []; const catCode = categoryCode(mainFilter); const visible = posts.filter((p) => { if (catCode) { const pc = (p.category || p.Category || "").toString().toUpperCase(); if (pc !== catCode) return false; } if (!matchesSubFilter(p, subFilter)) return false; if (!matchesTimeFilter(p.created_at || p.CreatedAt, tf)) return false; return true; }); visible.forEach((post) => createMarkerForPost(post, mapRef, markersRef, expandedElRef)); setVisiblePosts(visible); setStatus(visible.length ? "" : "No posts found."); }, [mapRef, markersRef, expandedElRef, mainFilter, subFilter] ); useEffect(() => { rebuildMarkersForFilters(timeFilter); }, [timeFilter, mainFilter, subFilter, rebuildMarkersForFilters]); useEffect(() => { const map = mapRef.current; if (!map) return; if (!viewParams.center) return; let cancelled = false; async function load() { const mapObj = mapRef.current; // Delay fetch while popup is open (prevents marker clear/rebuild) try { const until = mapObj?.__swIgnoreFetchUntil || 0; if (until && Date.now() < until) { const wait = Math.min(4000, until - Date.now()); if (delayedFetchRef.current) clearTimeout(delayedFetchRef.current); delayedFetchRef.current = setTimeout(load, wait + 25); return; } } catch {} const [lng, lat] = viewParams.center; const radiusKm = viewParams.radiusKm || 750; const filterKey = `${mainFilter}|${subFilter}`; const last = lastFetchRef.current; if (last.center && last.filterKey === filterKey) { const [lastLng, lastLat] = last.center; const distKm = haversineKm(lastLat, lastLng, viewParams.center[1], lng); const lastR = last.radiusKm || 0; const radiusChanged = !lastR || Math.abs(radiusKm - lastR) / Math.max(lastR, 1) >= 0.25; const minMoveKm = Math.max(50, radiusKm * 0.25); if (!radiusChanged && distKm < minMoveKm) return; } try { setStatus("Loading posts..."); setLoadingPosts(true); setLoadError(""); const catCode = categoryCode(mainFilter); const subCatParam = subFilter && subFilter.toUpperCase() !== "ALL" && subFilter !== "All" ? subFilter : ""; const newPosts = await fetchPosts({ category: catCode, subCategory: subCatParam, time: "", lat, lon: lng, radiusKm, }); if (cancelled) return; const existing = allPostsRef.current || []; const byId = new Map(); for (const p of existing) { if (p && typeof p.id !== "undefined") byId.set(p.id, p); } if (Array.isArray(newPosts)) { for (const p of newPosts) { if (p && typeof p.id !== "undefined" && !byId.has(p.id)) byId.set(p.id, p); } } allPostsRef.current = Array.from(byId.values()); lastFetchRef.current = { center: [...viewParams.center], radiusKm, filterKey }; rebuildMarkersForFilters(timeFilter); setLoadingPosts(false); } catch (err) { console.error("Erreur chargement posts:", err); if (!cancelled) { setStatus("Error loading posts."); setLoadError("Error loading posts."); setLoadingPosts(false); } } } load(); return () => { cancelled = true; if (delayedFetchRef.current) { try { clearTimeout(delayedFetchRef.current); } catch {} delayedFetchRef.current = null; } }; }, [viewParams, mainFilter, subFilter, mapRef, rebuildMarkersForFilters, timeFilter]); const handleIncomingPost = useCallback( (p) => { allPostsRef.current = [...allPostsRef.current, p]; const catCode = categoryCode(mainFilter); if (catCode) { const pc = (p.category || p.Category || "").toString().toUpperCase(); if (pc !== catCode) return; } if (!matchesSubFilter(p, subFilter)) return; if (!matchesTimeFilter(p.created_at || p.CreatedAt, timeFilter)) return; createMarkerForPost(p, mapRef, markersRef, expandedElRef); setVisiblePosts((current) => [...current, p]); }, [mainFilter, subFilter, timeFilter, mapRef, markersRef, expandedElRef] ); return { status, visiblePosts, loadingPosts, loadError, handleIncomingPost }; }