Auto deploy: 2025-12-09 04:27:52
This commit is contained in:
parent
ecf2e54778
commit
b86efc525a
|
|
@ -1,165 +1,23 @@
|
|||
import React, { useEffect, useRef, useState } from "react";
|
||||
import maplibregl from "maplibre-gl";
|
||||
import "maplibregl/dist/maplibre-gl.css";
|
||||
import "../../styles/maplibre.css"; // ✅ CSS locale copiée via bash
|
||||
|
||||
import { fetchPosts, fetchIpLocation, createPost } from "../../api/client";
|
||||
|
||||
/* ------------ CONSTANTES LOCALSTORAGE -------------- */
|
||||
const LAST_POS_KEY = "sociowire:lastPosition"; // position utilisateur (GPS/IP)
|
||||
const LAST_VIEW_KEY = "sociowire:lastView"; // dernière vue carte (centre+zoom)
|
||||
|
||||
/* ------------ UTILS GEOMETRIE -------------- */
|
||||
|
||||
// Haversine pour calculer un rayon approx (km)
|
||||
function haversineKm(lat1, lon1, lat2, lon2) {
|
||||
const R = 6371;
|
||||
const toRad = (deg) => (deg * Math.PI) / 180;
|
||||
const dLat = toRad(lat2 - lat1);
|
||||
const dLon = toRad(lat2 - lon1);
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(toRad(lat1)) *
|
||||
Math.cos(toRad(lat2)) *
|
||||
Math.sin(dLon / 2) *
|
||||
Math.sin(dLon / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
return R * c;
|
||||
}
|
||||
|
||||
// centre + rayon (km) de la vue actuelle
|
||||
function getViewFromMap(map) {
|
||||
const bounds = map.getBounds();
|
||||
const center = bounds.getCenter();
|
||||
const ne = bounds.getNorthEast();
|
||||
let radiusKm = haversineKm(center.lat, center.lng, ne.lat, ne.lng);
|
||||
if (!Number.isFinite(radiusKm) || radiusKm <= 0) {
|
||||
radiusKm = 250;
|
||||
}
|
||||
radiusKm = Math.min(Math.max(radiusKm, 10), 1000); // clamp 10–1000 km
|
||||
return {
|
||||
center: [center.lng, center.lat],
|
||||
radiusKm,
|
||||
};
|
||||
}
|
||||
|
||||
// extraction [lng, lat] depuis un post
|
||||
function getCoords(post) {
|
||||
const lat =
|
||||
post.lat ??
|
||||
post.latitude ??
|
||||
post.location?.lat ??
|
||||
(Array.isArray(post.coords) ? post.coords[1] : null);
|
||||
|
||||
const lon =
|
||||
post.lon ??
|
||||
post.lng ??
|
||||
post.location?.lng ??
|
||||
(Array.isArray(post.coords) ? post.coords[0] : null);
|
||||
|
||||
if (typeof lat === "number" && typeof lon === "number") return [lon, lat];
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ------------ UTILS CATEGORIES & TEMPS -------------- */
|
||||
|
||||
// Boutons de filtre principaux (droite)
|
||||
const FILTER_MAIN_CATEGORIES = ["All", "News", "Friends", "Events", "Market"];
|
||||
|
||||
// pour le panneau du bas (sous-cats) – AVEC "All"
|
||||
const FILTER_CATEGORY_MAP = {
|
||||
All: ["All"],
|
||||
News: ["All", "World", "Politics", "Tech", "Finance", "Local"],
|
||||
Friends: ["All", "Nearby", "Chats", "Groups", "Stories", "Favs"],
|
||||
Events: ["All", "Today", "Weekend", "Music", "Sports", "Local"],
|
||||
Market: ["All", "Actu", "Tech", "Finan", "Sport", "Deals"],
|
||||
};
|
||||
|
||||
// pour NEW POST seulement (PAS de "All")
|
||||
const CREATE_CATEGORY_MAP = {
|
||||
News: ["World", "Politics", "Tech", "Finance", "Local"],
|
||||
Friends: ["Nearby", "Chats", "Groups", "Stories", "Favs"],
|
||||
Events: ["Today", "Weekend", "Music", "Sports", "Local"],
|
||||
Market: ["Actu", "Tech", "Finan", "Sport", "Deals"],
|
||||
};
|
||||
|
||||
// map bouton -> code backend
|
||||
function categoryCode(mainFilter) {
|
||||
switch (mainFilter) {
|
||||
case "News":
|
||||
return "NEWS";
|
||||
case "Friends":
|
||||
return "FRIENDS";
|
||||
case "Events":
|
||||
return "EVENT"; // backend: EVENT
|
||||
case "Market":
|
||||
return "MARKET";
|
||||
case "All":
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Transforme un post en petits textes pour les bulles
|
||||
function extract(post) {
|
||||
const title = post.title || post.text || post.body || "Post";
|
||||
const body =
|
||||
post.snippet ||
|
||||
post.body ||
|
||||
post.content ||
|
||||
post.text ||
|
||||
"No additional details.";
|
||||
const short = title.length > 28 ? title.slice(0, 25) + "..." : title;
|
||||
return { title, body, short };
|
||||
}
|
||||
|
||||
// sous-catégorie: "All" = tout
|
||||
function matchesSubFilter(post, subFilter) {
|
||||
if (!subFilter || subFilter.toUpperCase() === "ALL" || subFilter === "All") {
|
||||
return true;
|
||||
}
|
||||
const raw = post.sub_category ?? post.subCategory ?? "";
|
||||
const sub = raw.toString().toLowerCase();
|
||||
return sub === subFilter.toLowerCase();
|
||||
}
|
||||
|
||||
// time filter local – règles :
|
||||
// Live/NOW = 10 minutes
|
||||
// Today/TODAY = 24 heures
|
||||
// Recently/RECENT = 4 heures
|
||||
// Past/PAST = plus vieux que 24 h
|
||||
function matchesTimeFilter(createdAt, timeFilter) {
|
||||
if (!timeFilter) return true;
|
||||
if (!createdAt) return true;
|
||||
|
||||
let d;
|
||||
if (typeof createdAt === "string") {
|
||||
if (createdAt.includes(" ")) {
|
||||
d = new Date(createdAt.replace(" ", "T") + "Z");
|
||||
} else {
|
||||
d = new Date(createdAt);
|
||||
}
|
||||
} else {
|
||||
d = new Date(createdAt);
|
||||
}
|
||||
if (Number.isNaN(d.getTime())) return true;
|
||||
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - d.getTime();
|
||||
const diffMin = diffMs / 60000;
|
||||
const diffHours = diffMs / 3600000;
|
||||
|
||||
switch (timeFilter) {
|
||||
case "NOW": // Live = 10 minutes
|
||||
return diffMin <= 10;
|
||||
case "TODAY": // 24h
|
||||
return diffHours <= 24;
|
||||
case "RECENT": // 4h
|
||||
return diffHours <= 4;
|
||||
case "PAST": // plus vieux que 24h
|
||||
return diffHours > 24;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
import {
|
||||
LAST_POS_KEY,
|
||||
LAST_VIEW_KEY,
|
||||
FILTER_MAIN_CATEGORIES,
|
||||
FILTER_CATEGORY_MAP,
|
||||
CREATE_CATEGORY_MAP,
|
||||
} from "./mapConfig";
|
||||
import { getViewFromMap } from "./mapGeo";
|
||||
import {
|
||||
categoryCode,
|
||||
matchesSubFilter,
|
||||
matchesTimeFilter,
|
||||
} from "./mapFilter";
|
||||
import { createMarkerForPost, clearAllMarkers } from "./markerManager";
|
||||
|
||||
/* =====================================================
|
||||
COMPONENT
|
||||
|
|
@ -167,11 +25,10 @@ function matchesTimeFilter(createdAt, timeFilter) {
|
|||
export default function MapView() {
|
||||
const containerRef = useRef(null);
|
||||
const mapRef = useRef(null);
|
||||
const markersRef = useRef([]); // [{ id, marker, el, compactHTML, expandedHTML }]
|
||||
const markersRef = useRef([]); // [{ id, marker, el, ... }]
|
||||
const expandedElRef = useRef(null);
|
||||
|
||||
// posts bruts (sans filtre temps) pour la zone + cat + sous-cat
|
||||
const allPostsRef = useRef([]);
|
||||
const allPostsRef = useRef([]); // posts bruts (sans filtre temps)
|
||||
|
||||
const [status, setStatus] = useState("Loading posts...");
|
||||
|
||||
|
|
@ -196,7 +53,7 @@ export default function MapView() {
|
|||
const [draftSubCategory, setDraftSubCategory] = useState(
|
||||
CREATE_CATEGORY_MAP.News[0]
|
||||
);
|
||||
const [draftCoords, setDraftCoords] = useState(null); // [lng, lat]
|
||||
const [draftCoords, setDraftCoords] = useState(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveError, setSaveError] = useState("");
|
||||
|
||||
|
|
@ -206,7 +63,6 @@ export default function MapView() {
|
|||
|
||||
const map = new maplibregl.Map({
|
||||
container: containerRef.current,
|
||||
// Thème nuit bleu CARTO + villes
|
||||
style: {
|
||||
version: 8,
|
||||
sources: {
|
||||
|
|
@ -234,7 +90,7 @@ export default function MapView() {
|
|||
antialias: true,
|
||||
});
|
||||
|
||||
// si on a une dernière vue en localStorage, on la remet ici
|
||||
// Reprise dernière vue
|
||||
let hadLastView = false;
|
||||
try {
|
||||
const raw = localStorage.getItem(LAST_VIEW_KEY);
|
||||
|
|
@ -250,9 +106,7 @@ export default function MapView() {
|
|||
hadLastView = true;
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
} catch (_) {}
|
||||
setHasLastView(hadLastView);
|
||||
|
||||
map.addControl(new maplibregl.NavigationControl(), "top-right");
|
||||
|
|
@ -264,7 +118,7 @@ export default function MapView() {
|
|||
setViewParams(vp);
|
||||
});
|
||||
|
||||
// à chaque fin de move/zoom :
|
||||
// update viewParams à chaque move
|
||||
map.on("moveend", () => {
|
||||
const vp = getViewFromMap(map);
|
||||
setViewParams(vp);
|
||||
|
|
@ -282,8 +136,7 @@ export default function MapView() {
|
|||
});
|
||||
|
||||
return () => {
|
||||
markersRef.current.forEach((m) => m.marker.remove());
|
||||
markersRef.current = [];
|
||||
clearAllMarkers(markersRef, expandedElRef);
|
||||
map.remove();
|
||||
mapRef.current = null;
|
||||
};
|
||||
|
|
@ -304,7 +157,6 @@ export default function MapView() {
|
|||
localStorage.setItem(LAST_POS_KEY, JSON.stringify({ lat, lon }));
|
||||
} catch (_) {}
|
||||
|
||||
// si on N'A PAS de lastView, on utilise le geo pour centrer
|
||||
if (!hasLastView) {
|
||||
map.flyTo({
|
||||
center: coords,
|
||||
|
|
@ -316,7 +168,7 @@ export default function MapView() {
|
|||
}
|
||||
}
|
||||
|
||||
// 1) dernière position utilisateur (pas la vue)
|
||||
// 1) dernière position user
|
||||
try {
|
||||
const raw = localStorage.getItem(LAST_POS_KEY);
|
||||
if (raw) {
|
||||
|
|
@ -372,111 +224,12 @@ export default function MapView() {
|
|||
}
|
||||
}, [mainFilter, bottomCategories, subFilter]);
|
||||
|
||||
/* ------------ CRÉATION D'UN MARKER POUR UN POST -------------- */
|
||||
const createMarkerForPost = (post) => {
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
|
||||
const coords = getCoords(post);
|
||||
if (!coords) return;
|
||||
|
||||
// évite les doublons (même id déjà affiché)
|
||||
if (
|
||||
typeof post.id === "number" &&
|
||||
markersRef.current.some((m) => m.id === post.id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [lngP, latP] = coords;
|
||||
if (
|
||||
typeof lngP !== "number" ||
|
||||
typeof latP !== "number" ||
|
||||
Number.isNaN(lngP) ||
|
||||
Number.isNaN(latP)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { title, body, short } = extract(post);
|
||||
|
||||
const compactHTML = `
|
||||
<div class="marker-inner">
|
||||
<div class="marker-dot"></div>
|
||||
<span class="marker-text">${short}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const expandedHTML = `
|
||||
<div class="marker-expanded-inner">
|
||||
<div class="marker-expanded-header">
|
||||
<div class="marker-dot"></div>
|
||||
<span class="marker-expanded-title">${title}</span>
|
||||
</div>
|
||||
<div class="marker-expanded-body">
|
||||
${body}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const el = document.createElement("div");
|
||||
el.className = "post-marker post-marker-compact";
|
||||
el.innerHTML = compactHTML;
|
||||
|
||||
const marker = new maplibregl.Marker({
|
||||
element: el,
|
||||
anchor: "bottom",
|
||||
})
|
||||
.setLngLat(coords)
|
||||
.addTo(map);
|
||||
|
||||
const meta = {
|
||||
id: typeof post.id === "number" ? post.id : null,
|
||||
marker,
|
||||
el,
|
||||
compactHTML,
|
||||
expandedHTML,
|
||||
};
|
||||
markersRef.current.push(meta);
|
||||
|
||||
const toggle = () => {
|
||||
if (expandedElRef.current && expandedElRef.current !== el) {
|
||||
const prev = markersRef.current.find(
|
||||
(m) => m.el === expandedElRef.current
|
||||
);
|
||||
if (prev) {
|
||||
prev.el.className = "post-marker post-marker-compact";
|
||||
prev.el.innerHTML = prev.compactHTML;
|
||||
}
|
||||
}
|
||||
|
||||
if (expandedElRef.current === el) {
|
||||
el.className = "post-marker post-marker-compact";
|
||||
el.innerHTML = compactHTML;
|
||||
expandedElRef.current = null;
|
||||
} else {
|
||||
el.className = "post-marker post-marker-expanded";
|
||||
el.innerHTML = expandedHTML;
|
||||
expandedElRef.current = el;
|
||||
}
|
||||
};
|
||||
|
||||
el.addEventListener("click", toggle);
|
||||
el.addEventListener("touchend", (e) => {
|
||||
e.preventDefault();
|
||||
toggle();
|
||||
});
|
||||
};
|
||||
|
||||
/* ------------ RECONSTRUIRE LES MARKERS SELON LE TIME FILTER -------------- */
|
||||
const rebuildMarkersForTimeFilter = (tf) => {
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
|
||||
// on enlève tous les markers
|
||||
markersRef.current.forEach((m) => m.marker.remove());
|
||||
markersRef.current = [];
|
||||
expandedElRef.current = null;
|
||||
clearAllMarkers(markersRef, expandedElRef);
|
||||
|
||||
const posts = allPostsRef.current || [];
|
||||
|
||||
|
|
@ -485,7 +238,7 @@ export default function MapView() {
|
|||
);
|
||||
|
||||
visible.forEach((post) => {
|
||||
createMarkerForPost(post);
|
||||
createMarkerForPost(post, mapRef, markersRef, expandedElRef);
|
||||
});
|
||||
|
||||
setStatus(visible.length ? "" : "No posts found.");
|
||||
|
|
@ -512,7 +265,7 @@ export default function MapView() {
|
|||
? subFilter
|
||||
: "";
|
||||
|
||||
// on laisse "time" vide -> le backend renvoie tout pour cette zone/cat/sous-cat
|
||||
// time="" -> backend renvoie TOUT pour zone + cat + sous-cat
|
||||
const posts = await fetchPosts({
|
||||
category: catCode,
|
||||
subCategory: subCatParam,
|
||||
|
|
@ -526,7 +279,6 @@ export default function MapView() {
|
|||
|
||||
allPostsRef.current = Array.isArray(posts) ? posts : [];
|
||||
|
||||
// Puis on reconstruit les markers avec le timeFilter ACTUEL
|
||||
rebuildMarkersForTimeFilter(timeFilter);
|
||||
} catch (err) {
|
||||
console.error("Erreur chargement posts:", err);
|
||||
|
|
@ -572,7 +324,7 @@ export default function MapView() {
|
|||
if (msg.type === "new_post" && msg.post) {
|
||||
const p = msg.post;
|
||||
|
||||
// 1) catégorie
|
||||
// Filtrage live: cat + sous-cat + temps + zone
|
||||
const catCode = categoryCode(mainFilter);
|
||||
if (
|
||||
catCode &&
|
||||
|
|
@ -582,28 +334,12 @@ export default function MapView() {
|
|||
return;
|
||||
}
|
||||
|
||||
// 2) sous-catégorie
|
||||
if (!matchesSubFilter(p, subFilter)) return;
|
||||
|
||||
// 3) temps (10 min / 24h / 4h / >24h)
|
||||
if (!matchesTimeFilter(p.created_at || p.CreatedAt, timeFilter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 4) zone actuelle
|
||||
if (viewParams.center && viewParams.radiusKm) {
|
||||
const coords = getCoords(p);
|
||||
if (coords) {
|
||||
const [lngP, latP] = coords;
|
||||
const [lngC, latC] = viewParams.center;
|
||||
const dKm = haversineKm(latC, lngC, latP, lngP);
|
||||
if (dKm > viewParams.radiusKm * 1.2) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5) éviter doublon (dans la liste brute)
|
||||
// évite doublon dans la liste
|
||||
const id = typeof p.id === "number" ? p.id : null;
|
||||
if (
|
||||
id !== null &&
|
||||
|
|
@ -612,15 +348,12 @@ export default function MapView() {
|
|||
return;
|
||||
}
|
||||
|
||||
// 6) ajouter à la liste brute
|
||||
allPostsRef.current = [...allPostsRef.current, p];
|
||||
|
||||
// 7) et si il match le timeFilter courant, on l'ajoute visuellement
|
||||
createMarkerForPost(p);
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore
|
||||
// et on l’ajoute visuellement
|
||||
createMarkerForPost(p, mapRef, markersRef, expandedElRef);
|
||||
}
|
||||
} catch (_) {}
|
||||
};
|
||||
|
||||
ws.onerror = (e) => {
|
||||
|
|
@ -661,7 +394,6 @@ export default function MapView() {
|
|||
setDraftTitle("");
|
||||
setDraftBody("");
|
||||
|
||||
// pour NEW POST on n'a PAS de catégorie "All"
|
||||
const main = mainFilter === "All" ? "News" : mainFilter;
|
||||
setDraftCategory(main);
|
||||
const list = CREATE_CATEGORY_MAP[main] || CREATE_CATEGORY_MAP.News;
|
||||
|
|
@ -714,7 +446,6 @@ export default function MapView() {
|
|||
await createPost(payload);
|
||||
setIsSaving(false);
|
||||
setIsCreating(false);
|
||||
// le WS va pousser le nouveau post si il match les filtres
|
||||
} catch (e) {
|
||||
console.error("createPost error:", e);
|
||||
setIsSaving(false);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
export const LAST_POS_KEY = "sociowire:lastPosition"; // position GPS/IP user
|
||||
export const LAST_VIEW_KEY = "sociowire:lastView"; // centre+zoom carte
|
||||
|
||||
// Boutons de filtre principaux (droite)
|
||||
export const FILTER_MAIN_CATEGORIES = [
|
||||
"All",
|
||||
"News",
|
||||
"Friends",
|
||||
"Events",
|
||||
"Market",
|
||||
];
|
||||
|
||||
// Sous-catégories pour les filtres (UI du bas) – AVEC "All"
|
||||
export const FILTER_CATEGORY_MAP = {
|
||||
All: ["All"],
|
||||
News: ["All", "World", "Politics", "Tech", "Finance", "Local"],
|
||||
Friends: ["All", "Nearby", "Chats", "Groups", "Stories", "Favs"],
|
||||
Events: ["All", "Today", "Weekend", "Music", "Sports", "Local"],
|
||||
Market: ["All", "Actu", "Tech", "Finan", "Sport", "Deals"],
|
||||
};
|
||||
|
||||
// Sous-catégories pour la création de post – SANS "All"
|
||||
export const CREATE_CATEGORY_MAP = {
|
||||
News: ["World", "Politics", "Tech", "Finance", "Local"],
|
||||
Friends: ["Nearby", "Chats", "Groups", "Stories", "Favs"],
|
||||
Events: ["Today", "Weekend", "Music", "Sports", "Local"],
|
||||
Market: ["Actu", "Tech", "Finan", "Sport", "Deals"],
|
||||
};
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import { haversineKm } from "./mapGeo";
|
||||
|
||||
// map bouton -> code backend
|
||||
export function categoryCode(mainFilter) {
|
||||
switch (mainFilter) {
|
||||
case "News":
|
||||
return "NEWS";
|
||||
case "Friends":
|
||||
return "FRIENDS";
|
||||
case "Events":
|
||||
return "EVENT"; // backend: EVENT
|
||||
case "Market":
|
||||
return "MARKET";
|
||||
case "All":
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Transforme un post en petits textes pour les bulles
|
||||
export function extractPostText(post) {
|
||||
const title = post.title || post.text || post.body || "Post";
|
||||
const body =
|
||||
post.snippet ||
|
||||
post.body ||
|
||||
post.content ||
|
||||
post.text ||
|
||||
"No additional details.";
|
||||
const short = title.length > 28 ? title.slice(0, 25) + "..." : title;
|
||||
return { title, body, short };
|
||||
}
|
||||
|
||||
// sous-catégorie: "All" = tout
|
||||
export function matchesSubFilter(post, subFilter) {
|
||||
if (!subFilter || subFilter.toUpperCase() === "ALL" || subFilter === "All") {
|
||||
return true;
|
||||
}
|
||||
const raw = post.sub_category ?? post.subCategory ?? "";
|
||||
const sub = raw.toString().toLowerCase();
|
||||
return sub === subFilter.toLowerCase();
|
||||
}
|
||||
|
||||
// time filter local – règles :
|
||||
// Live/NOW = 10 minutes
|
||||
// Today/TODAY = 24 heures
|
||||
// Recently/RECENT = 4 heures
|
||||
// Past/PAST = plus vieux que 24 h
|
||||
export function matchesTimeFilter(createdAt, timeFilter) {
|
||||
if (!timeFilter) return true;
|
||||
if (!createdAt) return true;
|
||||
|
||||
let d;
|
||||
if (typeof createdAt === "string") {
|
||||
if (createdAt.includes(" ")) {
|
||||
d = new Date(createdAt.replace(" ", "T") + "Z");
|
||||
} else {
|
||||
d = new Date(createdAt);
|
||||
}
|
||||
} else {
|
||||
d = new Date(createdAt);
|
||||
}
|
||||
if (Number.isNaN(d.getTime())) return true;
|
||||
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - d.getTime();
|
||||
const diffMin = diffMs / 60000;
|
||||
const diffHours = diffMs / 3600000;
|
||||
|
||||
switch (timeFilter) {
|
||||
case "NOW": // Live = 10 minutes
|
||||
return diffMin <= 10;
|
||||
case "TODAY": // 24h
|
||||
return diffHours <= 24;
|
||||
case "RECENT": // 4h
|
||||
return diffHours <= 4;
|
||||
case "PAST": // plus vieux que 24h
|
||||
return diffHours > 24;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifie si un post est dans le rayon courant
|
||||
export function isInViewRadius(post, center, radiusKm) {
|
||||
if (!center || !radiusKm) return true;
|
||||
const [lngC, latC] = center;
|
||||
const coords = post.coords || [post.lon ?? post.lng, post.lat ?? post.latitude];
|
||||
const [lngP, latP] = coords;
|
||||
if (
|
||||
typeof lngP !== "number" ||
|
||||
typeof latP !== "number" ||
|
||||
Number.isNaN(lngP) ||
|
||||
Number.isNaN(latP)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const dKm = haversineKm(latC, lngC, latP, lngP);
|
||||
return dKm <= radiusKm * 1.2;
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
// Haversine pour calculer un rayon approx (km)
|
||||
export function haversineKm(lat1, lon1, lat2, lon2) {
|
||||
const R = 6371;
|
||||
const toRad = (deg) => (deg * Math.PI) / 180;
|
||||
const dLat = toRad(lat2 - lat1);
|
||||
const dLon = toRad(lon2 - lon1);
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(toRad(lat1)) *
|
||||
Math.cos(toRad(lat2)) *
|
||||
Math.sin(dLon / 2) *
|
||||
Math.sin(dLon / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
return R * c;
|
||||
}
|
||||
|
||||
// centre + rayon (km) de la vue actuelle
|
||||
export function getViewFromMap(map) {
|
||||
const bounds = map.getBounds();
|
||||
const center = bounds.getCenter();
|
||||
const ne = bounds.getNorthEast();
|
||||
let radiusKm = haversineKm(center.lat, center.lng, ne.lat, ne.lng);
|
||||
if (!Number.isFinite(radiusKm) || radiusKm <= 0) {
|
||||
radiusKm = 250;
|
||||
}
|
||||
radiusKm = Math.min(Math.max(radiusKm, 10), 1000); // clamp 10–1000 km
|
||||
return {
|
||||
center: [center.lng, center.lat],
|
||||
radiusKm,
|
||||
};
|
||||
}
|
||||
|
||||
// extraction [lng, lat] depuis un post
|
||||
export function getCoords(post) {
|
||||
const lat =
|
||||
post.lat ??
|
||||
post.latitude ??
|
||||
post.location?.lat ??
|
||||
(Array.isArray(post.coords) ? post.coords[1] : null);
|
||||
|
||||
const lon =
|
||||
post.lon ??
|
||||
post.lng ??
|
||||
post.location?.lng ??
|
||||
(Array.isArray(post.coords) ? post.coords[0] : null);
|
||||
|
||||
if (typeof lat === "number" && typeof lon === "number") return [lon, lat];
|
||||
return null;
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
import maplibregl from "maplibre-gl";
|
||||
import { getCoords } from "./mapGeo";
|
||||
import { extractPostText } from "./mapFilter";
|
||||
|
||||
// Supprime tous les markers actuels
|
||||
export function clearAllMarkers(markersRef, expandedElRef) {
|
||||
markersRef.current.forEach((m) => m.marker.remove());
|
||||
markersRef.current = [];
|
||||
if (expandedElRef) {
|
||||
expandedElRef.current = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Crée un marker pour un post
|
||||
export function createMarkerForPost(post, mapRef, markersRef, expandedElRef) {
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
|
||||
const coords = getCoords(post);
|
||||
if (!coords) return;
|
||||
|
||||
// évite les doublons (même id déjà affiché)
|
||||
if (
|
||||
typeof post.id === "number" &&
|
||||
markersRef.current.some((m) => m.id === post.id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [lngP, latP] = coords;
|
||||
if (
|
||||
typeof lngP !== "number" ||
|
||||
typeof latP !== "number" ||
|
||||
Number.isNaN(lngP) ||
|
||||
Number.isNaN(latP)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { title, body, short } = extractPostText(post);
|
||||
|
||||
const compactHTML = `
|
||||
<div class="marker-inner">
|
||||
<div class="marker-dot"></div>
|
||||
<span class="marker-text">${short}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const expandedHTML = `
|
||||
<div class="marker-expanded-inner">
|
||||
<div class="marker-expanded-header">
|
||||
<div class="marker-dot"></div>
|
||||
<span class="marker-expanded-title">${title}</span>
|
||||
</div>
|
||||
<div class="marker-expanded-body">
|
||||
${body}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const el = document.createElement("div");
|
||||
el.className = "post-marker post-marker-compact";
|
||||
el.innerHTML = compactHTML;
|
||||
|
||||
const marker = new maplibregl.Marker({
|
||||
element: el,
|
||||
anchor: "bottom",
|
||||
})
|
||||
.setLngLat(coords)
|
||||
.addTo(map);
|
||||
|
||||
const meta = {
|
||||
id: typeof post.id === "number" ? post.id : null,
|
||||
marker,
|
||||
el,
|
||||
compactHTML,
|
||||
expandedHTML,
|
||||
};
|
||||
markersRef.current.push(meta);
|
||||
|
||||
const toggle = () => {
|
||||
if (expandedElRef.current && expandedElRef.current !== el) {
|
||||
const prev = markersRef.current.find(
|
||||
(m) => m.el === expandedElRef.current
|
||||
);
|
||||
if (prev) {
|
||||
prev.el.className = "post-marker post-marker-compact";
|
||||
prev.el.innerHTML = prev.compactHTML;
|
||||
}
|
||||
}
|
||||
|
||||
if (expandedElRef.current === el) {
|
||||
el.className = "post-marker post-marker-compact";
|
||||
el.innerHTML = compactHTML;
|
||||
expandedElRef.current = null;
|
||||
} else {
|
||||
el.className = "post-marker post-marker-expanded";
|
||||
el.innerHTML = expandedHTML;
|
||||
expandedElRef.current = el;
|
||||
}
|
||||
};
|
||||
|
||||
el.addEventListener("click", toggle);
|
||||
el.addEventListener("touchend", (e) => {
|
||||
e.preventDefault();
|
||||
toggle();
|
||||
});
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue