diff --git a/fix.sh b/fix.sh new file mode 100644 index 0000000..399141a --- /dev/null +++ b/fix.sh @@ -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." diff --git a/package-lock.json b/package-lock.json index 5c72180..92bd153 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,7 +58,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1075,7 +1074,6 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1126,7 +1124,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1249,7 +1246,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1551,7 +1547,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2772,7 +2767,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2858,7 +2852,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -2868,7 +2861,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3171,7 +3163,6 @@ "integrity": "sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@oxc-project/runtime": "0.97.0", "fdir": "^6.5.0", @@ -3294,7 +3285,6 @@ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/components/Map/useMapCore.js b/src/components/Map/useMapCore.js index 7261fed..2387ece 100644 --- a/src/components/Map/useMapCore.js +++ b/src/components/Map/useMapCore.js @@ -4,7 +4,7 @@ import { LAST_VIEW_KEY } from "./mapConfig"; import { getViewFromMap } from "./mapGeo"; import { clearAllMarkers, applyOcclusionForExpanded } from "./markerManager"; -// renvoie le style MapLibre en fonction du thème +// MapLibre base style by theme function getBaseStyle(theme) { const t = theme || "dark"; let tiles; @@ -49,21 +49,31 @@ export function useMapCore(theme) { const markersRef = useRef([]); const expandedElRef = useRef(null); - // ✅ default plus large + // 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.__setExpanded) { - el.__setExpanded(false); - } + 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; @@ -77,7 +87,7 @@ export function useMapCore(theme) { antialias: true, }); - // Reprise dernière vue + // Restore last view if available let hadLastView = false; try { const raw = localStorage.getItem(LAST_VIEW_KEY); @@ -99,7 +109,7 @@ export function useMapCore(theme) { 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) + // Re-occlude markers when post expanded const reOcclude = () => { const el = expandedElRef.current; if (el) applyOcclusionForExpanded(markersRef, el); @@ -107,24 +117,21 @@ export function useMapCore(theme) { 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; - } - }); + // Close expanded when clicking/dragging map + map.on("click", () => closeExpandedIfAny()); + map.on("dragstart", () => closeExpandedIfAny()); - map.on("load", () => { - const vp = getViewFromMap(map); - setViewParams(vp); - }); + // ✅ 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", () => { - const vp = getViewFromMap(map); - setViewParams(vp); + update(); try { + const vp = getViewFromMap(map); const center = vp.center; localStorage.setItem( LAST_VIEW_KEY, @@ -137,9 +144,8 @@ export function useMapCore(theme) { } catch {} }); - // IMPORTANT: click/drag sur la map => fermer le gros post - map.on("click", () => closeExpandedIfAny()); - map.on("dragstart", () => closeExpandedIfAny()); + // Also do an early update next frame (helps first load) + requestAnimationFrame(update); return () => { clearAllMarkers(markersRef, expandedElRef); @@ -149,10 +155,20 @@ export function useMapCore(theme) { // 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 { diff --git a/src/components/Map/useMapCore.js.bak.20251213-143011 b/src/components/Map/useMapCore.js.bak.20251213-143011 new file mode 100644 index 0000000..7261fed --- /dev/null +++ b/src/components/Map/useMapCore.js.bak.20251213-143011 @@ -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, + }; +} diff --git a/src/components/Map/usePostsEngine.js b/src/components/Map/usePostsEngine.js index 2e15616..ce88eca 100644 --- a/src/components/Map/usePostsEngine.js +++ b/src/components/Map/usePostsEngine.js @@ -1,14 +1,17 @@ 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 { 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) + * 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, @@ -20,6 +23,7 @@ export function usePostsEngine({ expandedElRef, }) { const allPostsRef = useRef([]); + const lastFetchRef = useRef({ center: null, radiusKm: null, @@ -31,7 +35,6 @@ export function usePostsEngine({ 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; @@ -43,14 +46,11 @@ export function usePostsEngine({ 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; }); @@ -65,39 +65,46 @@ export function usePostsEngine({ [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}`; + // ✅ 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, 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 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); - // Si ni zoom ni move significatif => skip if (!radiusChanged && distKm < minMoveKm) { return; } @@ -129,11 +136,8 @@ export function usePostsEngine({ const byId = new Map(); for (const p of existing) { - if (p && typeof p.id !== "undefined") { - byId.set(p.id, p); - } + 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)) { @@ -145,12 +149,11 @@ export function usePostsEngine({ allPostsRef.current = Array.from(byId.values()); lastFetchRef.current = { - center: [...viewParams.center], + center: [...centerArr], radiusKm, filterKey, }; - // Après réponse du backend → on ré-applique les filtres rebuildMarkersForFilters(timeFilter); setLoadingPosts(false); } catch (err) { @@ -168,31 +171,19 @@ export function usePostsEngine({ return () => { cancelled = true; }; - }, [ - viewParams, - mainFilter, - subFilter, - mapRef, - rebuildMarkersForFilters, - timeFilter, - ]); + }, [viewParams, mainFilter, subFilter, timeFilter, mapRef, rebuildMarkersForFilters, markersRef, expandedElRef]); - // 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; - } + if (!matchesTimeFilter(p.created_at || p.CreatedAt, timeFilter)) return; createMarkerForPost(p, mapRef, markersRef, expandedElRef); setVisiblePosts((current) => [...current, p]); diff --git a/src/components/Map/usePostsEngine.js.bak.20251213-143011 b/src/components/Map/usePostsEngine.js.bak.20251213-143011 new file mode 100644 index 0000000..2e15616 --- /dev/null +++ b/src/components/Map/usePostsEngine.js.bak.20251213-143011 @@ -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, + }; +}