fix load map
This commit is contained in:
parent
bb7414a793
commit
caeaea97e8
|
|
@ -0,0 +1,427 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Run from frontend repo root
|
||||||
|
# (the folder that contains src/)
|
||||||
|
test -d "src" || { echo "❌ Run this from the FRONTEND repo root (where ./src exists)"; exit 1; }
|
||||||
|
|
||||||
|
TS="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
echo "✅ Patching SocioWire frontend (timestamp: $TS)"
|
||||||
|
|
||||||
|
# backup
|
||||||
|
for f in \
|
||||||
|
src/components/Map/useMapCore.js \
|
||||||
|
src/components/Map/usePostsEngine.js
|
||||||
|
do
|
||||||
|
if [ -f "$f" ]; then
|
||||||
|
cp -a "$f" "$f.bak.$TS"
|
||||||
|
echo "📦 Backup: $f -> $f.bak.$TS"
|
||||||
|
else
|
||||||
|
echo "⚠️ Missing file: $f (will be created)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# FILE: src/components/Map/useMapCore.js
|
||||||
|
# -----------------------------
|
||||||
|
mkdir -p src/components/Map
|
||||||
|
cat > src/components/Map/useMapCore.js <<'EOF'
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import maplibregl from "maplibre-gl";
|
||||||
|
import { LAST_VIEW_KEY } from "./mapConfig";
|
||||||
|
import { getViewFromMap } from "./mapGeo";
|
||||||
|
import { clearAllMarkers, applyOcclusionForExpanded } from "./markerManager";
|
||||||
|
|
||||||
|
// MapLibre base style by theme
|
||||||
|
function getBaseStyle(theme) {
|
||||||
|
const t = theme || "dark";
|
||||||
|
let tiles;
|
||||||
|
|
||||||
|
switch (t) {
|
||||||
|
case "light":
|
||||||
|
tiles = ["https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"];
|
||||||
|
break;
|
||||||
|
case "blue":
|
||||||
|
tiles = ["https://a.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png"];
|
||||||
|
break;
|
||||||
|
case "dark":
|
||||||
|
default:
|
||||||
|
tiles = ["https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png"];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: 8,
|
||||||
|
sources: {
|
||||||
|
"carto-base": {
|
||||||
|
type: "raster",
|
||||||
|
tiles,
|
||||||
|
tileSize: 256,
|
||||||
|
attribution: "© OpenStreetMap contributors © CARTO",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: "carto-base-layer",
|
||||||
|
type: "raster",
|
||||||
|
source: "carto-base",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMapCore(theme) {
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const mapRef = useRef(null);
|
||||||
|
|
||||||
|
const markersRef = useRef([]);
|
||||||
|
const expandedElRef = useRef(null);
|
||||||
|
|
||||||
|
// default large radius
|
||||||
|
const [viewParams, setViewParams] = useState({
|
||||||
|
center: null,
|
||||||
|
radiusKm: 750,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [hasLastView, setHasLastView] = useState(false);
|
||||||
|
|
||||||
|
function closeExpandedIfAny() {
|
||||||
|
const el = expandedElRef.current;
|
||||||
|
if (el && el.__renderCompact) el.__renderCompact();
|
||||||
|
expandedElRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeUpdateViewParams(map) {
|
||||||
|
try {
|
||||||
|
const vp = getViewFromMap(map);
|
||||||
|
if (vp && Array.isArray(vp.center) && vp.center.length === 2) {
|
||||||
|
setViewParams(vp);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
const map = new maplibregl.Map({
|
||||||
|
container: containerRef.current,
|
||||||
|
style: getBaseStyle(theme || "dark"),
|
||||||
|
center: [-95, 40],
|
||||||
|
zoom: 3.5,
|
||||||
|
pitch: 0,
|
||||||
|
bearing: 0,
|
||||||
|
antialias: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore last view if available
|
||||||
|
let hadLastView = false;
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(LAST_VIEW_KEY);
|
||||||
|
if (raw) {
|
||||||
|
const v = JSON.parse(raw);
|
||||||
|
if (
|
||||||
|
typeof v.lat === "number" &&
|
||||||
|
typeof v.lon === "number" &&
|
||||||
|
typeof v.zoom === "number"
|
||||||
|
) {
|
||||||
|
map.setCenter([v.lon, v.lat]);
|
||||||
|
map.setZoom(v.zoom);
|
||||||
|
hadLastView = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
setHasLastView(hadLastView);
|
||||||
|
|
||||||
|
map.addControl(new maplibregl.NavigationControl(), "top-right");
|
||||||
|
mapRef.current = map;
|
||||||
|
|
||||||
|
// Re-occlude markers when post expanded
|
||||||
|
const reOcclude = () => {
|
||||||
|
const el = expandedElRef.current;
|
||||||
|
if (el) applyOcclusionForExpanded(markersRef, el);
|
||||||
|
};
|
||||||
|
map.on("move", reOcclude);
|
||||||
|
map.on("zoom", reOcclude);
|
||||||
|
|
||||||
|
// Close expanded when clicking/dragging map
|
||||||
|
map.on("click", () => closeExpandedIfAny());
|
||||||
|
map.on("dragstart", () => closeExpandedIfAny());
|
||||||
|
|
||||||
|
// ✅ CRITICAL FIX:
|
||||||
|
// Set view params ASAP and also on load/idle/moveend.
|
||||||
|
// On some devices/prod, relying on 'load' only can miss initial fetch triggers.
|
||||||
|
const update = () => safeUpdateViewParams(map);
|
||||||
|
|
||||||
|
map.on("load", update);
|
||||||
|
map.on("idle", update);
|
||||||
|
map.on("moveend", () => {
|
||||||
|
update();
|
||||||
|
try {
|
||||||
|
const vp = getViewFromMap(map);
|
||||||
|
const center = vp.center;
|
||||||
|
localStorage.setItem(
|
||||||
|
LAST_VIEW_KEY,
|
||||||
|
JSON.stringify({
|
||||||
|
lat: center[1],
|
||||||
|
lon: center[0],
|
||||||
|
zoom: map.getZoom(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also do an early update next frame (helps first load)
|
||||||
|
requestAnimationFrame(update);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearAllMarkers(markersRef, expandedElRef);
|
||||||
|
map.remove();
|
||||||
|
mapRef.current = null;
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Theme style switch
|
||||||
|
useEffect(() => {
|
||||||
|
const map = mapRef.current;
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
// setStyle triggers style reload; we still want viewParams updated after
|
||||||
|
map.setStyle(getBaseStyle(theme || "dark"));
|
||||||
|
|
||||||
|
// update view params after style swap (next frame)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
try {
|
||||||
|
safeUpdateViewParams(map);
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
containerRef,
|
||||||
|
mapRef,
|
||||||
|
markersRef,
|
||||||
|
expandedElRef,
|
||||||
|
viewParams,
|
||||||
|
hasLastView,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# FILE: src/components/Map/usePostsEngine.js
|
||||||
|
# -----------------------------
|
||||||
|
cat > src/components/Map/usePostsEngine.js <<'EOF'
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { fetchPosts } from "../../api/client"; // keep existing endpoint for now
|
||||||
|
import { categoryCode, matchesSubFilter, matchesTimeFilter } from "./mapFilter";
|
||||||
|
import { haversineKm } from "./mapGeo";
|
||||||
|
import { createMarkerForPost, clearAllMarkers } from "./markerManager";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Posts engine:
|
||||||
|
* - keeps a cache (allPostsRef)
|
||||||
|
* - applies filters (cat/sub/time) for map + Sociowall
|
||||||
|
* - fetches by zone
|
||||||
|
*
|
||||||
|
* ✅ FIX: first page load must fetch even if viewParams.center is null.
|
||||||
|
* We fallback to map.getCenter() and default radius 750.
|
||||||
|
*/
|
||||||
|
export function usePostsEngine({
|
||||||
|
mapRef,
|
||||||
|
viewParams,
|
||||||
|
mainFilter,
|
||||||
|
subFilter,
|
||||||
|
timeFilter,
|
||||||
|
markersRef,
|
||||||
|
expandedElRef,
|
||||||
|
}) {
|
||||||
|
const allPostsRef = useRef([]);
|
||||||
|
|
||||||
|
const lastFetchRef = useRef({
|
||||||
|
center: null,
|
||||||
|
radiusKm: null,
|
||||||
|
filterKey: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
// ✅ fallback center/radius if viewParams not ready yet
|
||||||
|
const centerArr =
|
||||||
|
(viewParams && Array.isArray(viewParams.center) && viewParams.center.length === 2)
|
||||||
|
? viewParams.center
|
||||||
|
: (() => {
|
||||||
|
const c = map.getCenter();
|
||||||
|
return [c.lng, c.lat];
|
||||||
|
})();
|
||||||
|
|
||||||
|
const radiusKm =
|
||||||
|
(viewParams && typeof viewParams.radiusKm === "number" && viewParams.radiusKm > 0)
|
||||||
|
? viewParams.radiusKm
|
||||||
|
: 750;
|
||||||
|
|
||||||
|
const [lng, lat] = centerArr;
|
||||||
|
|
||||||
|
const filterKey = `${mainFilter}|${subFilter}|${timeFilter}`;
|
||||||
|
const last = lastFetchRef.current;
|
||||||
|
|
||||||
|
if (last.center && last.filterKey === filterKey) {
|
||||||
|
const [lastLng, lastLat] = last.center;
|
||||||
|
const distKm = haversineKm(lastLat, lastLng, lat, 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: [...centerArr],
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}, [viewParams, mainFilter, subFilter, timeFilter, mapRef, rebuildMarkersForFilters, markersRef, expandedElRef]);
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "✅ Files written."
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Next:"
|
||||||
|
echo " npm run dev"
|
||||||
|
echo "or build/deploy as you do normally."
|
||||||
|
echo ""
|
||||||
|
echo "Tip: open DevTools Network -> you should see /api/posts on first page load."
|
||||||
|
|
@ -58,7 +58,6 @@
|
||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
|
|
@ -1075,7 +1074,6 @@
|
||||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
|
|
@ -1126,7 +1124,6 @@
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
|
|
@ -1249,7 +1246,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
|
|
@ -1551,7 +1547,6 @@
|
||||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
|
|
@ -2772,7 +2767,6 @@
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -2858,7 +2852,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
|
||||||
"integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
|
"integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
|
|
@ -2868,7 +2861,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz",
|
||||||
"integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
|
"integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
|
|
@ -3171,7 +3163,6 @@
|
||||||
"integrity": "sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA==",
|
"integrity": "sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/runtime": "0.97.0",
|
"@oxc-project/runtime": "0.97.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
|
|
@ -3294,7 +3285,6 @@
|
||||||
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
|
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { LAST_VIEW_KEY } from "./mapConfig";
|
||||||
import { getViewFromMap } from "./mapGeo";
|
import { getViewFromMap } from "./mapGeo";
|
||||||
import { clearAllMarkers, applyOcclusionForExpanded } from "./markerManager";
|
import { clearAllMarkers, applyOcclusionForExpanded } from "./markerManager";
|
||||||
|
|
||||||
// renvoie le style MapLibre en fonction du thème
|
// MapLibre base style by theme
|
||||||
function getBaseStyle(theme) {
|
function getBaseStyle(theme) {
|
||||||
const t = theme || "dark";
|
const t = theme || "dark";
|
||||||
let tiles;
|
let tiles;
|
||||||
|
|
@ -49,21 +49,31 @@ export function useMapCore(theme) {
|
||||||
const markersRef = useRef([]);
|
const markersRef = useRef([]);
|
||||||
const expandedElRef = useRef(null);
|
const expandedElRef = useRef(null);
|
||||||
|
|
||||||
// ✅ default plus large
|
// default large radius
|
||||||
const [viewParams, setViewParams] = useState({
|
const [viewParams, setViewParams] = useState({
|
||||||
center: null,
|
center: null,
|
||||||
radiusKm: 750,
|
radiusKm: 750,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [hasLastView, setHasLastView] = useState(false);
|
const [hasLastView, setHasLastView] = useState(false);
|
||||||
|
|
||||||
function closeExpandedIfAny() {
|
function closeExpandedIfAny() {
|
||||||
const el = expandedElRef.current;
|
const el = expandedElRef.current;
|
||||||
if (el && el.__setExpanded) {
|
if (el && el.__renderCompact) el.__renderCompact();
|
||||||
el.__setExpanded(false);
|
|
||||||
}
|
|
||||||
expandedElRef.current = null;
|
expandedElRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function safeUpdateViewParams(map) {
|
||||||
|
try {
|
||||||
|
const vp = getViewFromMap(map);
|
||||||
|
if (vp && Array.isArray(vp.center) && vp.center.length === 2) {
|
||||||
|
setViewParams(vp);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
|
@ -77,7 +87,7 @@ export function useMapCore(theme) {
|
||||||
antialias: true,
|
antialias: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reprise dernière vue
|
// Restore last view if available
|
||||||
let hadLastView = false;
|
let hadLastView = false;
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(LAST_VIEW_KEY);
|
const raw = localStorage.getItem(LAST_VIEW_KEY);
|
||||||
|
|
@ -99,7 +109,7 @@ export function useMapCore(theme) {
|
||||||
map.addControl(new maplibregl.NavigationControl(), "top-right");
|
map.addControl(new maplibregl.NavigationControl(), "top-right");
|
||||||
mapRef.current = map;
|
mapRef.current = map;
|
||||||
|
|
||||||
// If a post is expanded, re-apply occlusion on zoom/move (only hides overlapping ones)
|
// Re-occlude markers when post expanded
|
||||||
const reOcclude = () => {
|
const reOcclude = () => {
|
||||||
const el = expandedElRef.current;
|
const el = expandedElRef.current;
|
||||||
if (el) applyOcclusionForExpanded(markersRef, el);
|
if (el) applyOcclusionForExpanded(markersRef, el);
|
||||||
|
|
@ -107,24 +117,21 @@ export function useMapCore(theme) {
|
||||||
map.on("move", reOcclude);
|
map.on("move", reOcclude);
|
||||||
map.on("zoom", reOcclude);
|
map.on("zoom", reOcclude);
|
||||||
|
|
||||||
// Close expanded post when clicking on the map (outside markers)
|
// Close expanded when clicking/dragging map
|
||||||
map.on("click", () => {
|
map.on("click", () => closeExpandedIfAny());
|
||||||
const el = expandedElRef.current;
|
map.on("dragstart", () => closeExpandedIfAny());
|
||||||
if (el && el.__renderCompact) {
|
|
||||||
el.__renderCompact();
|
|
||||||
expandedElRef.current = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
map.on("load", () => {
|
// ✅ CRITICAL FIX:
|
||||||
const vp = getViewFromMap(map);
|
// Set view params ASAP and also on load/idle/moveend.
|
||||||
setViewParams(vp);
|
// On some devices/prod, relying on 'load' only can miss initial fetch triggers.
|
||||||
});
|
const update = () => safeUpdateViewParams(map);
|
||||||
|
|
||||||
|
map.on("load", update);
|
||||||
|
map.on("idle", update);
|
||||||
map.on("moveend", () => {
|
map.on("moveend", () => {
|
||||||
const vp = getViewFromMap(map);
|
update();
|
||||||
setViewParams(vp);
|
|
||||||
try {
|
try {
|
||||||
|
const vp = getViewFromMap(map);
|
||||||
const center = vp.center;
|
const center = vp.center;
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
LAST_VIEW_KEY,
|
LAST_VIEW_KEY,
|
||||||
|
|
@ -137,9 +144,8 @@ export function useMapCore(theme) {
|
||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
// IMPORTANT: click/drag sur la map => fermer le gros post
|
// Also do an early update next frame (helps first load)
|
||||||
map.on("click", () => closeExpandedIfAny());
|
requestAnimationFrame(update);
|
||||||
map.on("dragstart", () => closeExpandedIfAny());
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearAllMarkers(markersRef, expandedElRef);
|
clearAllMarkers(markersRef, expandedElRef);
|
||||||
|
|
@ -149,10 +155,20 @@ export function useMapCore(theme) {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Theme style switch
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
|
|
||||||
|
// setStyle triggers style reload; we still want viewParams updated after
|
||||||
map.setStyle(getBaseStyle(theme || "dark"));
|
map.setStyle(getBaseStyle(theme || "dark"));
|
||||||
|
|
||||||
|
// update view params after style swap (next frame)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
try {
|
||||||
|
safeUpdateViewParams(map);
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
}, [theme]);
|
}, [theme]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import maplibregl from "maplibre-gl";
|
||||||
|
import { LAST_VIEW_KEY } from "./mapConfig";
|
||||||
|
import { getViewFromMap } from "./mapGeo";
|
||||||
|
import { clearAllMarkers, applyOcclusionForExpanded } from "./markerManager";
|
||||||
|
|
||||||
|
// renvoie le style MapLibre en fonction du thème
|
||||||
|
function getBaseStyle(theme) {
|
||||||
|
const t = theme || "dark";
|
||||||
|
let tiles;
|
||||||
|
|
||||||
|
switch (t) {
|
||||||
|
case "light":
|
||||||
|
tiles = ["https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"];
|
||||||
|
break;
|
||||||
|
case "blue":
|
||||||
|
tiles = ["https://a.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png"];
|
||||||
|
break;
|
||||||
|
case "dark":
|
||||||
|
default:
|
||||||
|
tiles = ["https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png"];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: 8,
|
||||||
|
sources: {
|
||||||
|
"carto-base": {
|
||||||
|
type: "raster",
|
||||||
|
tiles,
|
||||||
|
tileSize: 256,
|
||||||
|
attribution: "© OpenStreetMap contributors © CARTO",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: "carto-base-layer",
|
||||||
|
type: "raster",
|
||||||
|
source: "carto-base",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMapCore(theme) {
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const mapRef = useRef(null);
|
||||||
|
|
||||||
|
const markersRef = useRef([]);
|
||||||
|
const expandedElRef = useRef(null);
|
||||||
|
|
||||||
|
// ✅ default plus large
|
||||||
|
const [viewParams, setViewParams] = useState({
|
||||||
|
center: null,
|
||||||
|
radiusKm: 750,
|
||||||
|
});
|
||||||
|
const [hasLastView, setHasLastView] = useState(false);
|
||||||
|
|
||||||
|
function closeExpandedIfAny() {
|
||||||
|
const el = expandedElRef.current;
|
||||||
|
if (el && el.__setExpanded) {
|
||||||
|
el.__setExpanded(false);
|
||||||
|
}
|
||||||
|
expandedElRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
const map = new maplibregl.Map({
|
||||||
|
container: containerRef.current,
|
||||||
|
style: getBaseStyle(theme || "dark"),
|
||||||
|
center: [-95, 40],
|
||||||
|
zoom: 3.5,
|
||||||
|
pitch: 0,
|
||||||
|
bearing: 0,
|
||||||
|
antialias: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reprise dernière vue
|
||||||
|
let hadLastView = false;
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(LAST_VIEW_KEY);
|
||||||
|
if (raw) {
|
||||||
|
const v = JSON.parse(raw);
|
||||||
|
if (
|
||||||
|
typeof v.lat === "number" &&
|
||||||
|
typeof v.lon === "number" &&
|
||||||
|
typeof v.zoom === "number"
|
||||||
|
) {
|
||||||
|
map.setCenter([v.lon, v.lat]);
|
||||||
|
map.setZoom(v.zoom);
|
||||||
|
hadLastView = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
setHasLastView(hadLastView);
|
||||||
|
|
||||||
|
map.addControl(new maplibregl.NavigationControl(), "top-right");
|
||||||
|
mapRef.current = map;
|
||||||
|
|
||||||
|
// If a post is expanded, re-apply occlusion on zoom/move (only hides overlapping ones)
|
||||||
|
const reOcclude = () => {
|
||||||
|
const el = expandedElRef.current;
|
||||||
|
if (el) applyOcclusionForExpanded(markersRef, el);
|
||||||
|
};
|
||||||
|
map.on("move", reOcclude);
|
||||||
|
map.on("zoom", reOcclude);
|
||||||
|
|
||||||
|
// Close expanded post when clicking on the map (outside markers)
|
||||||
|
map.on("click", () => {
|
||||||
|
const el = expandedElRef.current;
|
||||||
|
if (el && el.__renderCompact) {
|
||||||
|
el.__renderCompact();
|
||||||
|
expandedElRef.current = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
map.on("load", () => {
|
||||||
|
const vp = getViewFromMap(map);
|
||||||
|
setViewParams(vp);
|
||||||
|
});
|
||||||
|
|
||||||
|
map.on("moveend", () => {
|
||||||
|
const vp = getViewFromMap(map);
|
||||||
|
setViewParams(vp);
|
||||||
|
try {
|
||||||
|
const center = vp.center;
|
||||||
|
localStorage.setItem(
|
||||||
|
LAST_VIEW_KEY,
|
||||||
|
JSON.stringify({
|
||||||
|
lat: center[1],
|
||||||
|
lon: center[0],
|
||||||
|
zoom: map.getZoom(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
|
||||||
|
// IMPORTANT: click/drag sur la map => fermer le gros post
|
||||||
|
map.on("click", () => closeExpandedIfAny());
|
||||||
|
map.on("dragstart", () => closeExpandedIfAny());
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearAllMarkers(markersRef, expandedElRef);
|
||||||
|
map.remove();
|
||||||
|
mapRef.current = null;
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const map = mapRef.current;
|
||||||
|
if (!map) return;
|
||||||
|
map.setStyle(getBaseStyle(theme || "dark"));
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
containerRef,
|
||||||
|
mapRef,
|
||||||
|
markersRef,
|
||||||
|
expandedElRef,
|
||||||
|
viewParams,
|
||||||
|
hasLastView,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { fetchPosts } from "../../api/client";
|
import { fetchPosts } from "../../api/client"; // keep existing endpoint for now
|
||||||
import { categoryCode, matchesSubFilter, matchesTimeFilter } from "./mapFilter";
|
import { categoryCode, matchesSubFilter, matchesTimeFilter } from "./mapFilter";
|
||||||
import { haversineKm } from "./mapGeo";
|
import { haversineKm } from "./mapGeo";
|
||||||
import { createMarkerForPost, clearAllMarkers } from "./markerManager";
|
import { createMarkerForPost, clearAllMarkers } from "./markerManager";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gère :
|
* Posts engine:
|
||||||
* - posts en mémoire (allPostsRef)
|
* - keeps a cache (allPostsRef)
|
||||||
* - filtres (cat / sous-cat / temps) appliqués sur la map + Sociowall
|
* - applies filters (cat/sub/time) for map + Sociowall
|
||||||
* - fetch par zone (backend)
|
* - fetches by zone
|
||||||
|
*
|
||||||
|
* ✅ FIX: first page load must fetch even if viewParams.center is null.
|
||||||
|
* We fallback to map.getCenter() and default radius 750.
|
||||||
*/
|
*/
|
||||||
export function usePostsEngine({
|
export function usePostsEngine({
|
||||||
mapRef,
|
mapRef,
|
||||||
|
|
@ -20,6 +23,7 @@ export function usePostsEngine({
|
||||||
expandedElRef,
|
expandedElRef,
|
||||||
}) {
|
}) {
|
||||||
const allPostsRef = useRef([]);
|
const allPostsRef = useRef([]);
|
||||||
|
|
||||||
const lastFetchRef = useRef({
|
const lastFetchRef = useRef({
|
||||||
center: null,
|
center: null,
|
||||||
radiusKm: null,
|
radiusKm: null,
|
||||||
|
|
@ -31,7 +35,6 @@ export function usePostsEngine({
|
||||||
const [loadingPosts, setLoadingPosts] = useState(false);
|
const [loadingPosts, setLoadingPosts] = useState(false);
|
||||||
const [loadError, setLoadError] = useState("");
|
const [loadError, setLoadError] = useState("");
|
||||||
|
|
||||||
// Applique tous les filtres sur ce qu'on a déjà
|
|
||||||
const rebuildMarkersForFilters = useCallback(
|
const rebuildMarkersForFilters = useCallback(
|
||||||
(tf) => {
|
(tf) => {
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
|
|
@ -43,14 +46,11 @@ export function usePostsEngine({
|
||||||
const catCode = categoryCode(mainFilter);
|
const catCode = categoryCode(mainFilter);
|
||||||
|
|
||||||
const visible = posts.filter((p) => {
|
const visible = posts.filter((p) => {
|
||||||
// catégorie
|
|
||||||
if (catCode) {
|
if (catCode) {
|
||||||
const pc = (p.category || p.Category || "").toString().toUpperCase();
|
const pc = (p.category || p.Category || "").toString().toUpperCase();
|
||||||
if (pc !== catCode) return false;
|
if (pc !== catCode) return false;
|
||||||
}
|
}
|
||||||
// sous-catégorie
|
|
||||||
if (!matchesSubFilter(p, subFilter)) return false;
|
if (!matchesSubFilter(p, subFilter)) return false;
|
||||||
// temps
|
|
||||||
if (!matchesTimeFilter(p.created_at || p.CreatedAt, tf)) return false;
|
if (!matchesTimeFilter(p.created_at || p.CreatedAt, tf)) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
@ -65,39 +65,46 @@ export function usePostsEngine({
|
||||||
[mapRef, markersRef, expandedElRef, mainFilter, subFilter]
|
[mapRef, markersRef, expandedElRef, mainFilter, subFilter]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Changement de cat / sous-cat / temps → filtre instantané
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
rebuildMarkersForFilters(timeFilter);
|
rebuildMarkersForFilters(timeFilter);
|
||||||
}, [timeFilter, mainFilter, subFilter, rebuildMarkersForFilters]);
|
}, [timeFilter, mainFilter, subFilter, rebuildMarkersForFilters]);
|
||||||
|
|
||||||
// Fetch backend quand la vue bouge assez (OU quand le zoom change beaucoup le radius)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
if (!viewParams.center) return;
|
|
||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
const [lng, lat] = viewParams.center;
|
// ✅ fallback center/radius if viewParams not ready yet
|
||||||
const radiusKm = viewParams.radiusKm || 750;
|
const centerArr =
|
||||||
const filterKey = `${mainFilter}|${subFilter}`;
|
(viewParams && Array.isArray(viewParams.center) && viewParams.center.length === 2)
|
||||||
|
? viewParams.center
|
||||||
|
: (() => {
|
||||||
|
const c = map.getCenter();
|
||||||
|
return [c.lng, c.lat];
|
||||||
|
})();
|
||||||
|
|
||||||
|
const radiusKm =
|
||||||
|
(viewParams && typeof viewParams.radiusKm === "number" && viewParams.radiusKm > 0)
|
||||||
|
? viewParams.radiusKm
|
||||||
|
: 750;
|
||||||
|
|
||||||
|
const [lng, lat] = centerArr;
|
||||||
|
|
||||||
|
const filterKey = `${mainFilter}|${subFilter}|${timeFilter}`;
|
||||||
const last = lastFetchRef.current;
|
const last = lastFetchRef.current;
|
||||||
|
|
||||||
if (last.center && last.filterKey === filterKey) {
|
if (last.center && last.filterKey === filterKey) {
|
||||||
const [lastLng, lastLat] = last.center;
|
const [lastLng, lastLat] = last.center;
|
||||||
const distKm = haversineKm(lastLat, lastLng, viewParams.center[1], lng);
|
const distKm = haversineKm(lastLat, lastLng, lat, lng);
|
||||||
|
|
||||||
// ✅ refetch si le radius change pas mal (zoom in/out)
|
|
||||||
const lastR = last.radiusKm || 0;
|
const lastR = last.radiusKm || 0;
|
||||||
const radiusChanged =
|
const radiusChanged =
|
||||||
!lastR || Math.abs(radiusKm - lastR) / Math.max(lastR, 1) >= 0.25; // 25%
|
!lastR || Math.abs(radiusKm - lastR) / Math.max(lastR, 1) >= 0.25;
|
||||||
|
|
||||||
// move threshold moins agressif
|
|
||||||
const minMoveKm = Math.max(50, radiusKm * 0.25);
|
const minMoveKm = Math.max(50, radiusKm * 0.25);
|
||||||
|
|
||||||
// Si ni zoom ni move significatif => skip
|
|
||||||
if (!radiusChanged && distKm < minMoveKm) {
|
if (!radiusChanged && distKm < minMoveKm) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -129,11 +136,8 @@ export function usePostsEngine({
|
||||||
const byId = new Map();
|
const byId = new Map();
|
||||||
|
|
||||||
for (const p of existing) {
|
for (const p of existing) {
|
||||||
if (p && typeof p.id !== "undefined") {
|
if (p && typeof p.id !== "undefined") byId.set(p.id, p);
|
||||||
byId.set(p.id, p);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(newPosts)) {
|
if (Array.isArray(newPosts)) {
|
||||||
for (const p of newPosts) {
|
for (const p of newPosts) {
|
||||||
if (p && typeof p.id !== "undefined" && !byId.has(p.id)) {
|
if (p && typeof p.id !== "undefined" && !byId.has(p.id)) {
|
||||||
|
|
@ -145,12 +149,11 @@ export function usePostsEngine({
|
||||||
allPostsRef.current = Array.from(byId.values());
|
allPostsRef.current = Array.from(byId.values());
|
||||||
|
|
||||||
lastFetchRef.current = {
|
lastFetchRef.current = {
|
||||||
center: [...viewParams.center],
|
center: [...centerArr],
|
||||||
radiusKm,
|
radiusKm,
|
||||||
filterKey,
|
filterKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Après réponse du backend → on ré-applique les filtres
|
|
||||||
rebuildMarkersForFilters(timeFilter);
|
rebuildMarkersForFilters(timeFilter);
|
||||||
setLoadingPosts(false);
|
setLoadingPosts(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -168,31 +171,19 @@ export function usePostsEngine({
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [
|
}, [viewParams, mainFilter, subFilter, timeFilter, mapRef, rebuildMarkersForFilters, markersRef, expandedElRef]);
|
||||||
viewParams,
|
|
||||||
mainFilter,
|
|
||||||
subFilter,
|
|
||||||
mapRef,
|
|
||||||
rebuildMarkersForFilters,
|
|
||||||
timeFilter,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Nouveau post reçu via WebSocket
|
|
||||||
const handleIncomingPost = useCallback(
|
const handleIncomingPost = useCallback(
|
||||||
(p) => {
|
(p) => {
|
||||||
// ajoute au cache global
|
|
||||||
allPostsRef.current = [...allPostsRef.current, p];
|
allPostsRef.current = [...allPostsRef.current, p];
|
||||||
|
|
||||||
// si ça matche les filtres courants → on l'affiche
|
|
||||||
const catCode = categoryCode(mainFilter);
|
const catCode = categoryCode(mainFilter);
|
||||||
if (catCode) {
|
if (catCode) {
|
||||||
const pc = (p.category || p.Category || "").toString().toUpperCase();
|
const pc = (p.category || p.Category || "").toString().toUpperCase();
|
||||||
if (pc !== catCode) return;
|
if (pc !== catCode) return;
|
||||||
}
|
}
|
||||||
if (!matchesSubFilter(p, subFilter)) return;
|
if (!matchesSubFilter(p, subFilter)) return;
|
||||||
if (!matchesTimeFilter(p.created_at || p.CreatedAt, timeFilter)) {
|
if (!matchesTimeFilter(p.created_at || p.CreatedAt, timeFilter)) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
createMarkerForPost(p, mapRef, markersRef, expandedElRef);
|
createMarkerForPost(p, mapRef, markersRef, expandedElRef);
|
||||||
setVisiblePosts((current) => [...current, p]);
|
setVisiblePosts((current) => [...current, p]);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,210 @@
|
||||||
|
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";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère :
|
||||||
|
* - posts en mémoire (allPostsRef)
|
||||||
|
* - filtres (cat / sous-cat / temps) appliqués sur la map + Sociowall
|
||||||
|
* - fetch par zone (backend)
|
||||||
|
*/
|
||||||
|
export function usePostsEngine({
|
||||||
|
mapRef,
|
||||||
|
viewParams,
|
||||||
|
mainFilter,
|
||||||
|
subFilter,
|
||||||
|
timeFilter,
|
||||||
|
markersRef,
|
||||||
|
expandedElRef,
|
||||||
|
}) {
|
||||||
|
const allPostsRef = useRef([]);
|
||||||
|
const lastFetchRef = useRef({
|
||||||
|
center: null,
|
||||||
|
radiusKm: null,
|
||||||
|
filterKey: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [status, setStatus] = useState("Loading posts...");
|
||||||
|
const [visiblePosts, setVisiblePosts] = useState([]);
|
||||||
|
const [loadingPosts, setLoadingPosts] = useState(false);
|
||||||
|
const [loadError, setLoadError] = useState("");
|
||||||
|
|
||||||
|
// Applique tous les filtres sur ce qu'on a déjà
|
||||||
|
const rebuildMarkersForFilters = useCallback(
|
||||||
|
(tf) => {
|
||||||
|
const map = mapRef.current;
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
clearAllMarkers(markersRef, expandedElRef);
|
||||||
|
|
||||||
|
const posts = allPostsRef.current || [];
|
||||||
|
const catCode = categoryCode(mainFilter);
|
||||||
|
|
||||||
|
const visible = posts.filter((p) => {
|
||||||
|
// catégorie
|
||||||
|
if (catCode) {
|
||||||
|
const pc = (p.category || p.Category || "").toString().toUpperCase();
|
||||||
|
if (pc !== catCode) return false;
|
||||||
|
}
|
||||||
|
// sous-catégorie
|
||||||
|
if (!matchesSubFilter(p, subFilter)) return false;
|
||||||
|
// temps
|
||||||
|
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]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Changement de cat / sous-cat / temps → filtre instantané
|
||||||
|
useEffect(() => {
|
||||||
|
rebuildMarkersForFilters(timeFilter);
|
||||||
|
}, [timeFilter, mainFilter, subFilter, rebuildMarkersForFilters]);
|
||||||
|
|
||||||
|
// Fetch backend quand la vue bouge assez (OU quand le zoom change beaucoup le radius)
|
||||||
|
useEffect(() => {
|
||||||
|
const map = mapRef.current;
|
||||||
|
if (!map) return;
|
||||||
|
if (!viewParams.center) return;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
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);
|
||||||
|
|
||||||
|
// ✅ refetch si le radius change pas mal (zoom in/out)
|
||||||
|
const lastR = last.radiusKm || 0;
|
||||||
|
const radiusChanged =
|
||||||
|
!lastR || Math.abs(radiusKm - lastR) / Math.max(lastR, 1) >= 0.25; // 25%
|
||||||
|
|
||||||
|
// move threshold moins agressif
|
||||||
|
const minMoveKm = Math.max(50, radiusKm * 0.25);
|
||||||
|
|
||||||
|
// Si ni zoom ni move significatif => skip
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Après réponse du backend → on ré-applique les filtres
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
viewParams,
|
||||||
|
mainFilter,
|
||||||
|
subFilter,
|
||||||
|
mapRef,
|
||||||
|
rebuildMarkersForFilters,
|
||||||
|
timeFilter,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Nouveau post reçu via WebSocket
|
||||||
|
const handleIncomingPost = useCallback(
|
||||||
|
(p) => {
|
||||||
|
// ajoute au cache global
|
||||||
|
allPostsRef.current = [...allPostsRef.current, p];
|
||||||
|
|
||||||
|
// si ça matche les filtres courants → on l'affiche
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue