sw-fe/fix.sh

428 lines
11 KiB
Bash

#!/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."