update frontend

This commit is contained in:
Your Name 2025-12-13 02:42:20 -05:00
parent 68b859ccf5
commit 78f3ea5d63
6 changed files with 91 additions and 98 deletions

0
= Normal file
View File

0
b.bottom Normal file
View File

0
b.right Normal file
View File

View File

@ -33,7 +33,8 @@ function rectsOverlap(a, b) {
/** /**
* Hide only markers that overlap the expanded card. * Hide only markers that overlap the expanded card.
* - expandedEl: the marker root that is expanded * IMPORTANT: we hide ONLY markers that are BELOW the expanded card
* (so markers above stay visible).
*/ */
export function applyOcclusionForExpanded(markersRef, expandedEl) { export function applyOcclusionForExpanded(markersRef, expandedEl) {
if (!expandedEl) return; if (!expandedEl) return;
@ -41,9 +42,9 @@ export function applyOcclusionForExpanded(markersRef, expandedEl) {
const expandedCard = expandedEl.querySelector(".post-card"); const expandedCard = expandedEl.querySelector(".post-card");
if (!expandedCard) return; if (!expandedCard) return;
const PAD_X = 26; // left/right const PAD_X = 26; // left/right padding
const PAD_TOP = 20; // top const PAD_TOP = 20; // top padding
const PAD_BOTTOM = 95; // more bottom padding (image + actions + comments) const PAD_BOTTOM = 140; // EXTRA bottom padding (image + buttons + comments)
const raw = expandedCard.getBoundingClientRect(); const raw = expandedCard.getBoundingClientRect();
const expandedRect = { const expandedRect = {
@ -52,17 +53,24 @@ export function applyOcclusionForExpanded(markersRef, expandedEl) {
top: raw.top - PAD_TOP, top: raw.top - PAD_TOP,
bottom: raw.bottom + PAD_BOTTOM, bottom: raw.bottom + PAD_BOTTOM,
}; };
const list = markersRef.current || []; const list = markersRef.current || [];
for (const item of list) { for (const item of list) {
const el = item?.el; const el = item?.el;
if (!el || el === expandedEl) continue; if (!el || el === expandedEl) continue;
// Compute marker rect (bubble + pointer)
const r = el.getBoundingClientRect(); const r = el.getBoundingClientRect();
const hide = rectsOverlap(expandedRect, r); // ✅ only hide stuff that is BELOW the expanded card zone
const isBelow = r.top >= raw.top;
if (!isBelow) {
el.style.visibility = "visible";
el.style.pointerEvents = "auto";
continue;
}
const hide = rectsOverlap(expandedRect, r);
el.style.visibility = hide ? "hidden" : "visible"; el.style.visibility = hide ? "hidden" : "visible";
el.style.pointerEvents = hide ? "none" : "auto"; el.style.pointerEvents = hide ? "none" : "auto";
} }
@ -82,20 +90,15 @@ export function clearOcclusion(markersRef) {
} }
/** /**
* Crée un marqueur pour un post : * Crée un marker stable:
* - état compact : petite bulle + pointe * - root 0x0 anchored by MapLibre (anchor bottom)
* - état étendu : gros post (carte) * - UI is ABSOLUTE above the anchor, so expanded doesn't "shift" other markers
*
* Règles:
* - 1 seul post ouvert à la fois
* - clic sur la map => ferme (useMapCore)
* - quand un post est ouvert => on cache SEULEMENT les markers qui le recouvrent
*/ */
export function createMarkerForPost(post, mapRef, markersRef, expandedElRef) { export function createMarkerForPost(post, mapRef, markersRef, expandedElRef) {
const map = mapRef.current; const map = mapRef.current;
if (!map) return; if (!map) return;
// --- coordonnées --- // --- coords ---
const lat = const lat =
typeof post.lat === "number" typeof post.lat === "number"
? post.lat ? post.lat
@ -145,7 +148,6 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef) {
</div> </div>
<div class="post-pin-pointer-small"></div> <div class="post-pin-pointer-small"></div>
`; `;
// bring back hidden markers
clearOcclusion(markersRef); clearOcclusion(markersRef);
} }
@ -157,7 +159,7 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef) {
<div class="post-card-header"> <div class="post-card-header">
<div class="post-card-cat"> <div class="post-card-cat">
${escapeHtml(category || "NEWS")} ${escapeHtml(category || "NEWS")}
${subCat ? "· " + escapeHtml(subCat) : ""} ${subCat ? " · " + escapeHtml(subCat) : ""}
</div> </div>
<div class="post-card-time"> <div class="post-card-time">
${escapeHtml(created || "")} ${escapeHtml(created || "")}
@ -166,21 +168,15 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef) {
<div class="post-card-main"> <div class="post-card-main">
<div class="post-card-media"> <div class="post-card-media">
<div class="post-card-media-label"> <div class="post-card-media-label">Image / Vidéo</div>
Image / Vidéo
</div>
</div> </div>
<div class="post-card-info"> <div class="post-card-info">
<div class="post-card-title"> <div class="post-card-title">${escapeHtml(title)}</div>
${escapeHtml(title)}
</div>
<div class="post-card-meta"> <div class="post-card-meta">
by ${escapeHtml(author)} · lat ${lat.toFixed(3)} · lon ${lon.toFixed(3)} by ${escapeHtml(author)} · lat ${lat.toFixed(3)} · lon ${lon.toFixed(3)}
</div> </div>
<div class="post-card-body"> <div class="post-card-body">${escapeHtml(body)}</div>
${escapeHtml(body)}
</div>
</div> </div>
</div> </div>
@ -191,23 +187,20 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef) {
<button class="post-card-btn">Fix</button> <button class="post-card-btn">Fix</button>
</div> </div>
<div class="post-card-comments"> <div class="post-card-comments">Live chat / comments (placeholder)</div>
Live chat / comments (placeholder)
</div>
</div> </div>
<div class="post-card-pointer"></div> <div class="post-card-pointer"></div>
`; `;
// Wait a frame so DOM has final size, then hide only overlapping markers
requestAnimationFrame(() => { requestAnimationFrame(() => {
applyOcclusionForExpanded(markersRef, root); applyOcclusionForExpanded(markersRef, root);
}); });
} }
// état initial : compact // initial compact
renderCompact(); renderCompact();
// utilisé par useMapCore pour fermer via clic map // used by map click to close
root.__renderCompact = renderCompact; root.__renderCompact = renderCompact;
const marker = new maplibregl.Marker({ const marker = new maplibregl.Marker({
@ -222,7 +215,7 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef) {
root.addEventListener("click", (ev) => { root.addEventListener("click", (ev) => {
ev.stopPropagation(); ev.stopPropagation();
// Fermer l'ancien gros post // close previous expanded
if (expandedElRef.current && expandedElRef.current !== root) { if (expandedElRef.current && expandedElRef.current !== root) {
if (expandedElRef.current.__renderCompact) { if (expandedElRef.current.__renderCompact) {
expandedElRef.current.__renderCompact(); expandedElRef.current.__renderCompact();
@ -230,7 +223,6 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef) {
expandedElRef.current = null; expandedElRef.current = null;
} }
// Toggle
const isExpanded = root.classList.contains("post-pin--expanded"); const isExpanded = root.classList.contains("post-pin--expanded");
if (isExpanded) { if (isExpanded) {
renderCompact(); renderCompact();
@ -242,11 +234,11 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef) {
}); });
} }
/* petite fonction pour éviter dinjecter du HTML sale */ /* escape html */
function escapeHtml(str) { function escapeHtml(str) {
return String(str) return String(str)
.replace(/&/g, "&amp;") .replace(/&/g, "&amp;")
.replace(/</g, "&lt;") .replace(/</g, "&lt;")
.replace(/>/g, "&gt;") .replace(/>/g, "&gt;")
.replace(/"/g, "&quot;"); .replace(/"/g, "&quot;");
} }

View File

@ -4,7 +4,6 @@ import { LAST_VIEW_KEY } from "./mapConfig";
import { getViewFromMap } from "./mapGeo"; import { getViewFromMap } from "./mapGeo";
import { clearAllMarkers, applyOcclusionForExpanded } from "./markerManager"; import { clearAllMarkers, applyOcclusionForExpanded } from "./markerManager";
// renvoie le style MapLibre en fonction du thème
function getBaseStyle(theme) { function getBaseStyle(theme) {
const t = theme || "dark"; const t = theme || "dark";
let tiles; let tiles;
@ -32,13 +31,7 @@ function getBaseStyle(theme) {
attribution: "© OpenStreetMap contributors © CARTO", attribution: "© OpenStreetMap contributors © CARTO",
}, },
}, },
layers: [ layers: [{ id: "carto-base-layer", type: "raster", source: "carto-base" }],
{
id: "carto-base-layer",
type: "raster",
source: "carto-base",
},
],
}; };
} }
@ -49,16 +42,13 @@ export function useMapCore(theme) {
const markersRef = useRef([]); const markersRef = useRef([]);
const expandedElRef = useRef(null); const expandedElRef = useRef(null);
const [viewParams, setViewParams] = useState({ const [viewParams, setViewParams] = useState({ center: null, radiusKm: 250 });
center: null,
radiusKm: 250,
});
const [hasLastView, setHasLastView] = useState(false); const [hasLastView, setHasLastView] = useState(false);
function closeExpandedIfAny() { function closeExpandedIfAny() {
const el = expandedElRef.current; const el = expandedElRef.current;
if (el && el.__setExpanded) { if (el && el.__renderCompact) {
el.__setExpanded(false); el.__renderCompact();
} }
expandedElRef.current = null; expandedElRef.current = null;
} }
@ -76,7 +66,7 @@ export function useMapCore(theme) {
antialias: true, antialias: true,
}); });
// Reprise dernière vue // restore last view
let hadLastView = false; let hadLastView = false;
try { try {
const raw = localStorage.getItem(LAST_VIEW_KEY); const raw = localStorage.getItem(LAST_VIEW_KEY);
@ -98,7 +88,7 @@ export function useMapCore(theme) {
map.addControl(new maplibregl.NavigationControl(), "top-right"); map.addControl(new maplibregl.NavigationControl(), "top-right");
mapRef.current = map; mapRef.current = map;
// If a post is expanded, re-apply occlusion on zoom/move (only hides overlapping ones) // re-apply occlusion during move/zoom (only hides below overlapping ones)
const reOcclude = () => { const reOcclude = () => {
const el = expandedElRef.current; const el = expandedElRef.current;
if (el) applyOcclusionForExpanded(markersRef, el); if (el) applyOcclusionForExpanded(markersRef, el);
@ -106,18 +96,8 @@ export function useMapCore(theme) {
map.on("move", reOcclude); map.on("move", reOcclude);
map.on("zoom", reOcclude); map.on("zoom", reOcclude);
// Close expanded post when clicking on the map (outside markers)
map.on("click", () => {
const el = expandedElRef.current;
if (el && el.__renderCompact) {
el.__renderCompact();
expandedElRef.current = null;
}
});
map.on("load", () => { map.on("load", () => {
const vp = getViewFromMap(map); setViewParams(getViewFromMap(map));
setViewParams(vp);
}); });
map.on("moveend", () => { map.on("moveend", () => {
@ -127,16 +107,12 @@ export function useMapCore(theme) {
const center = vp.center; const center = vp.center;
localStorage.setItem( localStorage.setItem(
LAST_VIEW_KEY, LAST_VIEW_KEY,
JSON.stringify({ JSON.stringify({ lat: center[1], lon: center[0], zoom: map.getZoom() })
lat: center[1],
lon: center[0],
zoom: map.getZoom(),
})
); );
} catch {} } catch {}
}); });
// IMPORTANT: click/drag sur la map => fermer le gros post // ✅ close expanded when clicking/dragging map
map.on("click", () => closeExpandedIfAny()); map.on("click", () => closeExpandedIfAny());
map.on("dragstart", () => closeExpandedIfAny()); map.on("dragstart", () => closeExpandedIfAny());
@ -148,6 +124,7 @@ export function useMapCore(theme) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
// theme switch
useEffect(() => { useEffect(() => {
const map = mapRef.current; const map = mapRef.current;
if (!map) return; if (!map) return;
@ -162,4 +139,4 @@ export function useMapCore(theme) {
viewParams, viewParams,
hasLastView, hasLastView,
}; };
} }

View File

@ -1,23 +1,31 @@
/* MapLibre gère la position/transform via .maplibregl-marker, on ne touche PAS */ /* ==========================================================
SOCIOWIRE MARKERS ANCHOR STABLE (NO SHIFT)
Root = 0x0 on the exact geo point.
Bubble/Card are ABSOLUTE above the anchor.
========================================================== */
/* Root anchored by MapLibre at the geo point */
.post-pin { .post-pin {
display: flex; position: relative;
flex-direction: column; width: 0;
align-items: center; height: 0;
pointer-events: none; /* children clickable */
transform: translate3d(0, 0, 0);
}
.post-pin * {
pointer-events: auto; pointer-events: auto;
box-sizing: border-box;
/* IMPORTANT: NE PAS mettre position ici, sinon on casse l'ancrage MapLibre */
/* MapLibre applique position:absolute via .maplibregl-marker */
z-index: 1;
} }
/* quand expand => au-dessus de TOUT */ /* =========================
.post-pin--expanded { COMPACT
z-index: 99999; ========================= */
}
/* ===== ÉTAT COMPACT : petite bulle + petite pointe ===== */
.post-pin--compact .post-pin-bubble { .post-pin--compact .post-pin-bubble {
position: absolute;
left: 0;
top: 0;
transform: translate(-50%, calc(-100% - 12px));
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
padding: 4px 8px; padding: 4px 8px;
@ -25,6 +33,7 @@
background: rgba(6, 40, 80, 0.95); background: rgba(6, 40, 80, 0.95);
border: 1px solid rgba(80, 180, 255, 0.9); border: 1px solid rgba(80, 180, 255, 0.9);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
max-width: 180px;
} }
.post-pin-dot { .post-pin-dot {
@ -33,32 +42,45 @@
border-radius: 999px; border-radius: 999px;
background: #ff8a00; background: #ff8a00;
margin-right: 6px; margin-right: 6px;
flex-shrink: 0;
} }
.post-pin-text { .post-pin-text {
color: #e8f5ff; color: #e8f5ff;
font-size: 11px; font-size: 11px;
font-weight: 500; font-weight: 500;
max-width: 120px; max-width: 160px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
/* petite pointe */ /* little pointer tip */
.post-pin-pointer-small { .post-pin--compact .post-pin-pointer-small {
position: absolute;
left: 0;
top: 0;
transform: translate(-50%, -100%);
width: 0; width: 0;
height: 0; height: 0;
margin-top: 2px;
border-left: 7px solid transparent; border-left: 7px solid transparent;
border-right: 7px solid transparent; border-right: 7px solid transparent;
border-top: 9px solid rgba(6, 40, 80, 0.95); border-top: 9px solid rgba(6, 40, 80, 0.95);
filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.6)); filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.6));
} }
/* ===== ÉTAT ÉTENDU : gros post (carte) ===== */ /* =========================
EXPANDED
========================= */
.post-pin--expanded {
z-index: 999999;
}
.post-pin--expanded .post-card { .post-pin--expanded .post-card {
position: absolute;
left: 0;
top: 0;
transform: translate(-50%, calc(-100% - 14px));
min-width: 260px; min-width: 260px;
max-width: 320px; max-width: 320px;
background: rgba(8, 20, 40, 0.97); background: rgba(8, 20, 40, 0.97);
@ -67,7 +89,6 @@
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.8); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.8);
border: 1px solid rgba(90, 190, 255, 0.9); border: 1px solid rgba(90, 190, 255, 0.9);
color: #e8f5ff; color: #e8f5ff;
box-sizing: border-box;
} }
/* header : catégorie + heure/date */ /* header : catégorie + heure/date */
@ -96,7 +117,6 @@
margin-bottom: 6px; margin-bottom: 6px;
} }
/* fake bloc image/vidéo */
.post-card-media { .post-card-media {
width: 72px; width: 72px;
height: 72px; height: 72px;
@ -105,6 +125,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-shrink: 0;
} }
.post-card-media-label { .post-card-media-label {
@ -113,9 +134,9 @@
color: #e8f5ff; color: #e8f5ff;
} }
/* texte principal */
.post-card-info { .post-card-info {
flex: 1; flex: 1;
min-width: 0;
} }
.post-card-title { .post-card-title {
@ -142,6 +163,7 @@
display: flex; display: flex;
gap: 6px; gap: 6px;
margin-bottom: 6px; margin-bottom: 6px;
flex-wrap: wrap;
} }
.post-card-btn { .post-card-btn {
@ -154,7 +176,6 @@
cursor: pointer; cursor: pointer;
} }
/* zone commentaires */
.post-card-comments { .post-card-comments {
font-size: 10px; font-size: 10px;
color: #9eb7ff; color: #9eb7ff;
@ -163,13 +184,16 @@
background: rgba(3, 14, 32, 0.95); background: rgba(3, 14, 32, 0.95);
} }
/* Pointe sous le gros post */ /* pointer under expanded card */
.post-card-pointer { .post-pin--expanded .post-card-pointer {
position: absolute;
left: 0;
top: 0;
transform: translate(-50%, -100%);
width: 0; width: 0;
height: 0; height: 0;
margin-top: 3px;
border-left: 10px solid transparent; border-left: 10px solid transparent;
border-right: 10px solid transparent; border-right: 10px solid transparent;
border-top: 11px solid rgba(8, 20, 40, 0.97); border-top: 11px solid rgba(8, 20, 40, 0.97);
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.8)); filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.8));
} }