Auto deploy: 2025-12-09 05:35:44
This commit is contained in:
parent
057826feb0
commit
7c0509466c
|
|
@ -1,7 +1,8 @@
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import maplibregl from "maplibre-gl";
|
import maplibregl from "maplibre-gl";
|
||||||
import "../../styles/maplibre.css"; // CSS locale copiée depuis maplibre-gl
|
import "../../styles/maplibre.css";
|
||||||
import "../../styles/mapMarkers.css";
|
import "../../styles/mapMarkers.css";
|
||||||
|
|
||||||
import { fetchPosts, fetchIpLocation, createPost } from "../../api/client";
|
import { fetchPosts, fetchIpLocation, createPost } from "../../api/client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -19,34 +20,30 @@ import {
|
||||||
} from "./mapFilter";
|
} from "./mapFilter";
|
||||||
import { createMarkerForPost, clearAllMarkers } from "./markerManager";
|
import { createMarkerForPost, clearAllMarkers } from "./markerManager";
|
||||||
|
|
||||||
/* =====================================================
|
|
||||||
COMPONENT
|
|
||||||
===================================================== */
|
|
||||||
export default function MapView() {
|
export default function MapView() {
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
const mapRef = useRef(null);
|
const mapRef = useRef(null);
|
||||||
const markersRef = useRef([]); // [{ id, marker, el, ... }]
|
const markersRef = useRef([]); // [{ id, marker, el, ... }]
|
||||||
const expandedElRef = useRef(null);
|
const expandedElRef = useRef(null);
|
||||||
|
|
||||||
// posts bruts accumulés (sans filtre temps)
|
// posts bruts accumulés
|
||||||
const allPostsRef = useRef([]);
|
const allPostsRef = useRef([]);
|
||||||
|
|
||||||
// dernière zone fetchée (centre/rayon + filtres) pour éviter de spam le backend
|
// dernière zone fetchée
|
||||||
const lastFetchRef = useRef({
|
const lastFetchRef = useRef({
|
||||||
center: null, // [lng, lat]
|
center: null, // [lng, lat]
|
||||||
radiusKm: null,
|
radiusKm: null,
|
||||||
filterKey: "", // `${mainFilter}|${subFilter}`
|
filterKey: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const [status, setStatus] = useState("Loading posts...");
|
const [status, setStatus] = useState("Loading posts...");
|
||||||
|
|
||||||
const [mainFilter, setMainFilter] = useState("All"); // All/News/Friends/Events/Market
|
const [mainFilter, setMainFilter] = useState("All"); // All/News/Friends/Events/Market
|
||||||
const [timeFilter, setTimeFilter] = useState("RECENT"); // NOW/TODAY/RECENT/PAST
|
const [timeFilter, setTimeFilter] = useState("RECENT"); // NOW/TODAY/RECENT/PAST
|
||||||
const [subFilter, setSubFilter] = useState("All"); // "All" ou sous-cat
|
const [subFilter, setSubFilter] = useState("All");
|
||||||
|
|
||||||
const [userPosition, setUserPosition] = useState(null); // [lng, lat]
|
const [userPosition, setUserPosition] = useState(null); // [lng, lat]
|
||||||
|
|
||||||
// juste pour mémoriser centre/zoom et les sauver dans localStorage
|
|
||||||
const [viewParams, setViewParams] = useState({
|
const [viewParams, setViewParams] = useState({
|
||||||
center: null,
|
center: null,
|
||||||
radiusKm: 250,
|
radiusKm: 250,
|
||||||
|
|
@ -66,7 +63,8 @@ export default function MapView() {
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [saveError, setSaveError] = useState("");
|
const [saveError, setSaveError] = useState("");
|
||||||
|
|
||||||
/* ------------ INIT MAP (THEME BLEU NUIT + LAST VIEW) -------------- */
|
/* ================= INIT MAP ================= */
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
|
@ -87,6 +85,7 @@ export default function MapView() {
|
||||||
layers: [
|
layers: [
|
||||||
{
|
{
|
||||||
id: "carto-dark-layer",
|
id: "carto-dark-layer",
|
||||||
|
type: "raster",
|
||||||
source: "carto-dark",
|
source: "carto-dark",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -114,19 +113,19 @@ export default function MapView() {
|
||||||
hadLastView = true;
|
hadLastView = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
setHasLastView(hadLastView);
|
setHasLastView(hadLastView);
|
||||||
|
|
||||||
map.addControl(new maplibregl.NavigationControl(), "top-right");
|
map.addControl(new maplibregl.NavigationControl(), "top-right");
|
||||||
mapRef.current = map;
|
mapRef.current = map;
|
||||||
|
|
||||||
// première vue → viewParams
|
|
||||||
map.on("load", () => {
|
map.on("load", () => {
|
||||||
const vp = getViewFromMap(map);
|
const vp = getViewFromMap(map);
|
||||||
setViewParams(vp);
|
setViewParams(vp);
|
||||||
});
|
});
|
||||||
|
|
||||||
// update viewParams à chaque move (pour sauvegarder vue + servir de zone)
|
|
||||||
map.on("moveend", () => {
|
map.on("moveend", () => {
|
||||||
const vp = getViewFromMap(map);
|
const vp = getViewFromMap(map);
|
||||||
setViewParams(vp);
|
setViewParams(vp);
|
||||||
|
|
@ -140,7 +139,9 @@ export default function MapView() {
|
||||||
zoom: map.getZoom(),
|
zoom: map.getZoom(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} catch (_) {}
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -150,7 +151,8 @@ export default function MapView() {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/* ------------ POSITION UTILISATEUR (GPS/IP) -------------- */
|
/* ================= POSITION UTILISATEUR (GPS / IP) ================= */
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
|
|
@ -163,7 +165,9 @@ export default function MapView() {
|
||||||
setUserPosition(coords);
|
setUserPosition(coords);
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(LAST_POS_KEY, JSON.stringify({ lat, lon }));
|
localStorage.setItem(LAST_POS_KEY, JSON.stringify({ lat, lon }));
|
||||||
} catch (_) {}
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasLastView) {
|
if (!hasLastView) {
|
||||||
map.flyTo({
|
map.flyTo({
|
||||||
|
|
@ -185,7 +189,9 @@ export default function MapView() {
|
||||||
setUserPosition([p.lon, p.lat]);
|
setUserPosition([p.lon, p.lat]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
// 2) GPS → 3) IP backend
|
// 2) GPS → 3) IP backend
|
||||||
function viaIpFallback() {
|
function viaIpFallback() {
|
||||||
|
|
@ -222,7 +228,8 @@ export default function MapView() {
|
||||||
};
|
};
|
||||||
}, [hasLastView]);
|
}, [hasLastView]);
|
||||||
|
|
||||||
/* ------------ SOUS-CATEGORIES (bas) -------------- */
|
/* ================= FILTRES (bas) ================= */
|
||||||
|
|
||||||
const bottomCategories = FILTER_CATEGORY_MAP[mainFilter] || ["All"];
|
const bottomCategories = FILTER_CATEGORY_MAP[mainFilter] || ["All"];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -232,7 +239,8 @@ export default function MapView() {
|
||||||
}
|
}
|
||||||
}, [mainFilter, bottomCategories, subFilter]);
|
}, [mainFilter, bottomCategories, subFilter]);
|
||||||
|
|
||||||
/* ------------ RECONSTRUIRE LES MARKERS SELON LE TIME FILTER -------------- */
|
/* ================= MARKERS & TIME FILTER ================= */
|
||||||
|
|
||||||
const rebuildMarkersForTimeFilter = (tf) => {
|
const rebuildMarkersForTimeFilter = (tf) => {
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
|
|
@ -252,7 +260,8 @@ export default function MapView() {
|
||||||
setStatus(visible.length ? "" : "No posts found.");
|
setStatus(visible.length ? "" : "No posts found.");
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ------------ LOAD POSTS PAR ZONE + FILTRES (ACCUMULATIF) -------------- */
|
/* ================= FETCH PAR ZONE (ACCUMULATIF) ================= */
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
|
|
@ -267,13 +276,11 @@ export default function MapView() {
|
||||||
|
|
||||||
const last = lastFetchRef.current;
|
const last = lastFetchRef.current;
|
||||||
|
|
||||||
// Si les filtres n'ont pas changé, on regarde la distance
|
|
||||||
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, viewParams.center[1], lng);
|
||||||
|
|
||||||
// Distance trop petite → même zone -> pas de refetch
|
const minMoveKm = Math.max(100, radiusKm * 0.4);
|
||||||
const minMoveKm = Math.max(100, radiusKm * 0.4); // ex: 100 km ou 40% du rayon
|
|
||||||
if (distKm < minMoveKm) {
|
if (distKm < minMoveKm) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -288,11 +295,10 @@ export default function MapView() {
|
||||||
? subFilter
|
? subFilter
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
// ici on utilise lat/lon/radius pour que le backend renvoie les posts de la zone
|
|
||||||
const newPosts = await fetchPosts({
|
const newPosts = await fetchPosts({
|
||||||
category: catCode,
|
category: catCode,
|
||||||
subCategory: subCatParam,
|
subCategory: subCatParam,
|
||||||
time: "", // on gère le temps en front
|
time: "",
|
||||||
lat,
|
lat,
|
||||||
lon: lng,
|
lon: lng,
|
||||||
radiusKm,
|
radiusKm,
|
||||||
|
|
@ -303,14 +309,12 @@ export default function MapView() {
|
||||||
const existing = allPostsRef.current || [];
|
const existing = allPostsRef.current || [];
|
||||||
const byId = new Map();
|
const byId = new Map();
|
||||||
|
|
||||||
// garde tout ce qu'on avait déjà
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ajoute les nouveaux posts (sans écraser ceux déjà présents)
|
|
||||||
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)) {
|
||||||
|
|
@ -321,7 +325,6 @@ export default function MapView() {
|
||||||
|
|
||||||
allPostsRef.current = Array.from(byId.values());
|
allPostsRef.current = Array.from(byId.values());
|
||||||
|
|
||||||
// On se souvient de la dernière zone fetchée
|
|
||||||
lastFetchRef.current = {
|
lastFetchRef.current = {
|
||||||
center: [...viewParams.center],
|
center: [...viewParams.center],
|
||||||
radiusKm,
|
radiusKm,
|
||||||
|
|
@ -340,20 +343,16 @@ export default function MapView() {
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
// on refetch SI:
|
|
||||||
// - la vue change assez (seuil calculé)
|
|
||||||
// - OU filtres changent
|
|
||||||
// (PAS quand timeFilter change, lui ne fait que rebuild côté front)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [viewParams, mainFilter, subFilter]);
|
}, [viewParams, mainFilter, subFilter]);
|
||||||
|
|
||||||
/* ------------ QUAND LE TIME FILTER CHANGE, ON RECONSTRUIT JUSTE LES MARKERS -------------- */
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
rebuildMarkersForTimeFilter(timeFilter);
|
rebuildMarkersForTimeFilter(timeFilter);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [timeFilter]);
|
}, [timeFilter]);
|
||||||
|
|
||||||
/* ------------ WEBSOCKET : AJOUTE JUSTE LES POSTS QUI MATCH LES FILTRES -------------- */
|
/* ================= WEBSOCKET ================= */
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const proto = window.location.protocol === "https:" ? "wss" : "ws";
|
const proto = window.location.protocol === "https:" ? "wss" : "ws";
|
||||||
const host =
|
const host =
|
||||||
|
|
@ -377,7 +376,6 @@ export default function MapView() {
|
||||||
if (msg.type === "new_post" && msg.post) {
|
if (msg.type === "new_post" && msg.post) {
|
||||||
const p = msg.post;
|
const p = msg.post;
|
||||||
|
|
||||||
// Filtrage live: cat + sous-cat + temps
|
|
||||||
const catCode = categoryCode(mainFilter);
|
const catCode = categoryCode(mainFilter);
|
||||||
if (
|
if (
|
||||||
catCode &&
|
catCode &&
|
||||||
|
|
@ -392,7 +390,6 @@ export default function MapView() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// éviter doublon dans la liste
|
|
||||||
const id = typeof p.id === "number" ? p.id : null;
|
const id = typeof p.id === "number" ? p.id : null;
|
||||||
if (
|
if (
|
||||||
id !== null &&
|
id !== null &&
|
||||||
|
|
@ -403,10 +400,11 @@ export default function MapView() {
|
||||||
|
|
||||||
allPostsRef.current = [...allPostsRef.current, p];
|
allPostsRef.current = [...allPostsRef.current, p];
|
||||||
|
|
||||||
// et on l’ajoute visuellement
|
|
||||||
createMarkerForPost(p, mapRef, markersRef, expandedElRef);
|
createMarkerForPost(p, mapRef, markersRef, expandedElRef);
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = (e) => {
|
ws.onerror = (e) => {
|
||||||
|
|
@ -420,7 +418,7 @@ export default function MapView() {
|
||||||
};
|
};
|
||||||
}, [mainFilter, subFilter, timeFilter]);
|
}, [mainFilter, subFilter, timeFilter]);
|
||||||
|
|
||||||
/* ------------ ACTIONS UI -------------- */
|
/* ================= ACTIONS UI ================= */
|
||||||
|
|
||||||
const handleFlyToMe = () => {
|
const handleFlyToMe = () => {
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
|
|
@ -506,7 +504,7 @@ export default function MapView() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ------------ RENDER -------------- */
|
/* ================= RENDER ================= */
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="map-view">
|
<div className="map-view">
|
||||||
|
|
@ -547,7 +545,7 @@ export default function MapView() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* RIGHT — catégories principales avec All */}
|
{/* RIGHT — main categories */}
|
||||||
<div className="map-overlay map-overlay-right">
|
<div className="map-overlay map-overlay-right">
|
||||||
{FILTER_MAIN_CATEGORIES.map((cat) => (
|
{FILTER_MAIN_CATEGORIES.map((cat) => (
|
||||||
<button
|
<button
|
||||||
|
|
@ -562,7 +560,7 @@ export default function MapView() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* BOTTOM — sous-cats + All */}
|
{/* BOTTOM — subcategories */}
|
||||||
<div className="map-overlay map-overlay-bottom">
|
<div className="map-overlay map-overlay-bottom">
|
||||||
{bottomCategories.map((c) => (
|
{bottomCategories.map((c) => (
|
||||||
<button
|
<button
|
||||||
|
|
@ -577,7 +575,7 @@ export default function MapView() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* BOUTON "MY SPOT" */}
|
{/* MY SPOT */}
|
||||||
<div className="map-overlay map-overlay-myloc">
|
<div className="map-overlay map-overlay-myloc">
|
||||||
<button
|
<button
|
||||||
className="chip-pill"
|
className="chip-pill"
|
||||||
|
|
@ -588,14 +586,14 @@ export default function MapView() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CROSSHAIR SUR LA MAP */}
|
{/* CROSSHAIR */}
|
||||||
{isCreating && (
|
{isCreating && (
|
||||||
<div className="map-overlay map-crosshair" onClick={handlePickCenter}>
|
<div className="map-overlay map-crosshair" onClick={handlePickCenter}>
|
||||||
<div className="crosshair-aim" />
|
<div className="crosshair-aim" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* PANEL CREATION DE POST */}
|
{/* CREATE POST PANEL */}
|
||||||
{isCreating && (
|
{isCreating && (
|
||||||
<div className="map-overlay create-post-panel">
|
<div className="map-overlay create-post-panel">
|
||||||
<div className="create-post-header">
|
<div className="create-post-header">
|
||||||
|
|
@ -672,6 +670,7 @@ export default function MapView() {
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
>
|
>
|
||||||
{isSaving ? "Posting..." : "Post as tommy"}
|
{isSaving ? "Posting..." : "Post as tommy"}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue