Auto deploy: 2025-12-09 05:35:44

This commit is contained in:
Your Name 2025-12-09 05:35:48 +00:00
parent 057826feb0
commit 7c0509466c
1 changed files with 46 additions and 47 deletions

View File

@ -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 lajoute 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>
)} )}