update frontend
This commit is contained in:
parent
2a66d974a2
commit
721e17d3a6
|
|
@ -1,59 +1,59 @@
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import maplibregl from "maplibre-gl";
|
|
||||||
import "maplibre-gl/dist/maplibre-gl.css";
|
import "maplibre-gl/dist/maplibre-gl.css";
|
||||||
import "../../styles/mapMarkers.css";
|
import "../../styles/mapMarkers.css";
|
||||||
import "../../styles/mapLayout.css";
|
import "../../styles/mapLayout.css";
|
||||||
|
|
||||||
import { fetchPosts, fetchIpLocation, createPost } from "../../api/client";
|
import { createPost } from "../../api/client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
LAST_POS_KEY,
|
|
||||||
LAST_VIEW_KEY,
|
|
||||||
FILTER_MAIN_CATEGORIES,
|
FILTER_MAIN_CATEGORIES,
|
||||||
FILTER_CATEGORY_MAP,
|
FILTER_CATEGORY_MAP,
|
||||||
CREATE_CATEGORY_MAP,
|
CREATE_CATEGORY_MAP,
|
||||||
} from "./mapConfig";
|
} from "./mapConfig";
|
||||||
import { getViewFromMap, haversineKm } from "./mapGeo";
|
import { useMapCore } from "./useMapCore";
|
||||||
import {
|
import { useUserPosition } from "./useUserPosition";
|
||||||
categoryCode,
|
import { usePostsEngine } from "./usePostsEngine";
|
||||||
matchesSubFilter,
|
|
||||||
matchesTimeFilter,
|
|
||||||
} from "./mapFilter";
|
|
||||||
import { createMarkerForPost, clearAllMarkers } from "./markerManager";
|
|
||||||
import PostList from "../Posts/PostList";
|
import PostList from "../Posts/PostList";
|
||||||
|
|
||||||
export default function MapView() {
|
export default function MapView() {
|
||||||
const containerRef = useRef(null);
|
// MAP + refs
|
||||||
const mapRef = useRef(null);
|
const {
|
||||||
const markersRef = useRef([]); // [{ id, marker, el, ... }]
|
containerRef,
|
||||||
const expandedElRef = useRef(null);
|
mapRef,
|
||||||
|
markersRef,
|
||||||
|
expandedElRef,
|
||||||
|
viewParams,
|
||||||
|
hasLastView,
|
||||||
|
} = useMapCore();
|
||||||
|
|
||||||
// posts bruts accumulés (tous ceux qu'on a déjà chargés)
|
// FILTRES
|
||||||
const allPostsRef = useRef([]);
|
const [mainFilter, setMainFilter] = useState("All");
|
||||||
|
const [timeFilter, setTimeFilter] = useState("RECENT");
|
||||||
// dernière zone fetchée
|
|
||||||
const lastFetchRef = useRef({
|
|
||||||
center: null, // [lng, lat]
|
|
||||||
radiusKm: null,
|
|
||||||
filterKey: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const [status, setStatus] = useState("Loading posts...");
|
|
||||||
|
|
||||||
const [mainFilter, setMainFilter] = useState("All"); // All/News/Friends/Events/Market
|
|
||||||
const [timeFilter, setTimeFilter] = useState("RECENT"); // NOW/TODAY/RECENT/PAST
|
|
||||||
const [subFilter, setSubFilter] = useState("All");
|
const [subFilter, setSubFilter] = useState("All");
|
||||||
|
|
||||||
const [userPosition, setUserPosition] = useState(null); // [lng, lat]
|
// USER POSITION
|
||||||
|
const userPosition = useUserPosition(mapRef, hasLastView);
|
||||||
|
|
||||||
const [viewParams, setViewParams] = useState({
|
// POSTS ENGINE (markers + Sociowall)
|
||||||
center: null,
|
const {
|
||||||
radiusKm: 250,
|
status,
|
||||||
|
visiblePosts,
|
||||||
|
loadingPosts,
|
||||||
|
loadError,
|
||||||
|
handleIncomingPost,
|
||||||
|
} = usePostsEngine({
|
||||||
|
mapRef,
|
||||||
|
viewParams,
|
||||||
|
mainFilter,
|
||||||
|
subFilter,
|
||||||
|
timeFilter,
|
||||||
|
markersRef,
|
||||||
|
expandedElRef,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [hasLastView, setHasLastView] = useState(false);
|
// SOCIOWALL selection
|
||||||
|
const [selectedPost, setSelectedPost] = useState(null);
|
||||||
|
|
||||||
// NEW POST
|
// NEW POST (overlay)
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
const [draftTitle, setDraftTitle] = useState("");
|
const [draftTitle, setDraftTitle] = useState("");
|
||||||
const [draftBody, setDraftBody] = useState("");
|
const [draftBody, setDraftBody] = useState("");
|
||||||
|
|
@ -65,177 +65,6 @@ export default function MapView() {
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [saveError, setSaveError] = useState("");
|
const [saveError, setSaveError] = useState("");
|
||||||
|
|
||||||
// SOCIOWALL
|
|
||||||
const [visiblePosts, setVisiblePosts] = useState([]);
|
|
||||||
const [selectedPost, setSelectedPost] = useState(null);
|
|
||||||
const [loadingPosts, setLoadingPosts] = useState(false);
|
|
||||||
const [loadError, setLoadError] = useState("");
|
|
||||||
|
|
||||||
/* ================= INIT MAP ================= */
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!containerRef.current) return;
|
|
||||||
|
|
||||||
const map = new maplibregl.Map({
|
|
||||||
container: containerRef.current,
|
|
||||||
style: {
|
|
||||||
version: 8,
|
|
||||||
sources: {
|
|
||||||
"carto-dark": {
|
|
||||||
type: "raster",
|
|
||||||
tiles: [
|
|
||||||
"https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",
|
|
||||||
],
|
|
||||||
tileSize: 256,
|
|
||||||
attribution: "© OpenStreetMap contributors © CARTO",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "carto-dark-layer",
|
|
||||||
type: "raster",
|
|
||||||
source: "carto-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 {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
setHasLastView(hadLastView);
|
|
||||||
|
|
||||||
map.addControl(new maplibregl.NavigationControl(), "top-right");
|
|
||||||
mapRef.current = map;
|
|
||||||
|
|
||||||
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 {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearAllMarkers(markersRef, expandedElRef);
|
|
||||||
map.remove();
|
|
||||||
mapRef.current = null;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
/* ================= POSITION UTILISATEUR (GPS / IP) ================= */
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const map = mapRef.current;
|
|
||||||
if (!map) return;
|
|
||||||
|
|
||||||
let cancelled = false;
|
|
||||||
|
|
||||||
function saveUserPos(lat, lon) {
|
|
||||||
if (cancelled) return;
|
|
||||||
const coords = [lon, lat];
|
|
||||||
setUserPosition(coords);
|
|
||||||
try {
|
|
||||||
localStorage.setItem(LAST_POS_KEY, JSON.stringify({ lat, lon }));
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasLastView) {
|
|
||||||
map.flyTo({
|
|
||||||
center: coords,
|
|
||||||
zoom: 9,
|
|
||||||
speed: 1.2,
|
|
||||||
curve: 1.5,
|
|
||||||
essential: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1) dernière position user
|
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem(LAST_POS_KEY);
|
|
||||||
if (raw) {
|
|
||||||
const p = JSON.parse(raw);
|
|
||||||
if (typeof p.lat === "number" && typeof p.lon === "number") {
|
|
||||||
setUserPosition([p.lon, p.lat]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) GPS → 3) IP backend
|
|
||||||
function viaIpFallback() {
|
|
||||||
(async () => {
|
|
||||||
const ipCoords = await fetchIpLocation();
|
|
||||||
if (!ipCoords || cancelled) return;
|
|
||||||
const [lon, lat] = ipCoords;
|
|
||||||
saveUserPos(lat, lon);
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("geolocation" in navigator) {
|
|
||||||
navigator.geolocation.getCurrentPosition(
|
|
||||||
(pos) => {
|
|
||||||
if (cancelled) return;
|
|
||||||
saveUserPos(pos.coords.latitude, pos.coords.longitude);
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
if (cancelled) return;
|
|
||||||
viaIpFallback();
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enableHighAccuracy: true,
|
|
||||||
timeout: 8000,
|
|
||||||
maximumAge: 30000,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
viaIpFallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [hasLastView]);
|
|
||||||
|
|
||||||
/* ================= FILTRES (bas) ================= */
|
/* ================= FILTRES (bas) ================= */
|
||||||
|
|
||||||
const bottomCategories = FILTER_CATEGORY_MAP[mainFilter] || ["All"];
|
const bottomCategories = FILTER_CATEGORY_MAP[mainFilter] || ["All"];
|
||||||
|
|
@ -247,146 +76,6 @@ export default function MapView() {
|
||||||
}
|
}
|
||||||
}, [mainFilter, bottomCategories, subFilter]);
|
}, [mainFilter, bottomCategories, subFilter]);
|
||||||
|
|
||||||
/* ================= MARKERS & FILTERS ================= */
|
|
||||||
|
|
||||||
// Applique TOUS les filtres (catégorie + sous-cat + temps) sur ce qu'on a déjà en mémoire.
|
|
||||||
const rebuildMarkersForFilters = (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.");
|
|
||||||
};
|
|
||||||
|
|
||||||
// ⚡ Dès qu'on change de catégorie ou sous-catégorie,
|
|
||||||
// on filtre immédiatement ce qui est déjà chargé
|
|
||||||
useEffect(() => {
|
|
||||||
rebuildMarkersForFilters(timeFilter);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [mainFilter, subFilter]);
|
|
||||||
|
|
||||||
/* ================= FETCH PAR ZONE (ACCUMULATIF) ================= */
|
|
||||||
|
|
||||||
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 || 250;
|
|
||||||
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 minMoveKm = Math.max(100, radiusKm * 0.4);
|
|
||||||
if (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,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Une fois le backend répondu, on met à jour les markers + Sociowall
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [viewParams, mainFilter, subFilter]);
|
|
||||||
|
|
||||||
// Quand on change le filtre temps, on réapplique directement
|
|
||||||
useEffect(() => {
|
|
||||||
rebuildMarkersForFilters(timeFilter);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [timeFilter]);
|
|
||||||
|
|
||||||
/* ================= WEBSOCKET ================= */
|
/* ================= WEBSOCKET ================= */
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -410,24 +99,7 @@ export default function MapView() {
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(ev.data);
|
const msg = JSON.parse(ev.data);
|
||||||
if (msg.type === "new_post" && msg.post) {
|
if (msg.type === "new_post" && msg.post) {
|
||||||
const p = msg.post;
|
handleIncomingPost(msg.post);
|
||||||
|
|
||||||
// On ajoute à la liste globale
|
|
||||||
allPostsRef.current = [...allPostsRef.current, p];
|
|
||||||
|
|
||||||
// Et si le nouveau post 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]);
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
|
|
@ -443,7 +115,7 @@ export default function MapView() {
|
||||||
ws.close();
|
ws.close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [mainFilter, subFilter, timeFilter]);
|
}, [handleIncomingPost]);
|
||||||
|
|
||||||
/* ================= ACTIONS UI ================= */
|
/* ================= ACTIONS UI ================= */
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import maplibregl from "maplibre-gl";
|
||||||
|
import { LAST_VIEW_KEY } from "./mapConfig";
|
||||||
|
import { getViewFromMap } from "./mapGeo";
|
||||||
|
import { clearAllMarkers } from "./markerManager";
|
||||||
|
|
||||||
|
export function useMapCore() {
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const mapRef = useRef(null);
|
||||||
|
|
||||||
|
// pour les markers (utilisé aussi par usePostsEngine + MapView)
|
||||||
|
const markersRef = useRef([]);
|
||||||
|
const expandedElRef = useRef(null);
|
||||||
|
|
||||||
|
const [viewParams, setViewParams] = useState({
|
||||||
|
center: null,
|
||||||
|
radiusKm: 250,
|
||||||
|
});
|
||||||
|
const [hasLastView, setHasLastView] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
const map = new maplibregl.Map({
|
||||||
|
container: containerRef.current,
|
||||||
|
style: {
|
||||||
|
version: 8,
|
||||||
|
sources: {
|
||||||
|
"carto-dark": {
|
||||||
|
type: "raster",
|
||||||
|
tiles: [
|
||||||
|
"https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",
|
||||||
|
],
|
||||||
|
tileSize: 256,
|
||||||
|
attribution: "© OpenStreetMap contributors © CARTO",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: "carto-dark-layer",
|
||||||
|
type: "raster",
|
||||||
|
source: "carto-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 {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
setHasLastView(hadLastView);
|
||||||
|
|
||||||
|
map.addControl(new maplibregl.NavigationControl(), "top-right");
|
||||||
|
mapRef.current = map;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearAllMarkers(markersRef, expandedElRef);
|
||||||
|
map.remove();
|
||||||
|
mapRef.current = null;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
containerRef,
|
||||||
|
mapRef,
|
||||||
|
markersRef,
|
||||||
|
expandedElRef,
|
||||||
|
viewParams,
|
||||||
|
hasLastView,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
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
|
||||||
|
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 || 250;
|
||||||
|
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 minMoveKm = Math.max(100, radiusKm * 0.4);
|
||||||
|
if (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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { LAST_POS_KEY } from "./mapConfig";
|
||||||
|
import { fetchIpLocation } from "../../api/client";
|
||||||
|
|
||||||
|
export function useUserPosition(mapRef, hasLastView) {
|
||||||
|
const [userPosition, setUserPosition] = useState(null); // [lng, lat]
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const map = mapRef.current;
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
function saveUserPos(lat, lon) {
|
||||||
|
if (cancelled) return;
|
||||||
|
const coords = [lon, lat];
|
||||||
|
setUserPosition(coords);
|
||||||
|
try {
|
||||||
|
localStorage.setItem(LAST_POS_KEY, JSON.stringify({ lat, lon }));
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasLastView) {
|
||||||
|
map.flyTo({
|
||||||
|
center: coords,
|
||||||
|
zoom: 9,
|
||||||
|
speed: 1.2,
|
||||||
|
curve: 1.5,
|
||||||
|
essential: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) dernière position user
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(LAST_POS_KEY);
|
||||||
|
if (raw) {
|
||||||
|
const p = JSON.parse(raw);
|
||||||
|
if (typeof p.lat === "number" && typeof p.lon === "number") {
|
||||||
|
setUserPosition([p.lon, p.lat]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) GPS → 3) IP backend
|
||||||
|
function viaIpFallback() {
|
||||||
|
(async () => {
|
||||||
|
const ipCoords = await fetchIpLocation();
|
||||||
|
if (!ipCoords || cancelled) return;
|
||||||
|
const [lon, lat] = ipCoords;
|
||||||
|
saveUserPos(lat, lon);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("geolocation" in navigator) {
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(pos) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
saveUserPos(pos.coords.latitude, pos.coords.longitude);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (cancelled) return;
|
||||||
|
viaIpFallback();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enableHighAccuracy: true,
|
||||||
|
timeout: 8000,
|
||||||
|
maximumAge: 30000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
viaIpFallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [mapRef, hasLastView]);
|
||||||
|
|
||||||
|
return userPosition;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue