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

View File

@ -4,7 +4,6 @@ import { LAST_VIEW_KEY } from "./mapConfig";
import { getViewFromMap } from "./mapGeo";
import { clearAllMarkers, applyOcclusionForExpanded } from "./markerManager";
// renvoie le style MapLibre en fonction du thème
function getBaseStyle(theme) {
const t = theme || "dark";
let tiles;
@ -32,13 +31,7 @@ function getBaseStyle(theme) {
attribution: "© OpenStreetMap contributors © CARTO",
},
},
layers: [
{
id: "carto-base-layer",
type: "raster",
source: "carto-base",
},
],
layers: [{ id: "carto-base-layer", type: "raster", source: "carto-base" }],
};
}
@ -49,16 +42,13 @@ export function useMapCore(theme) {
const markersRef = useRef([]);
const expandedElRef = useRef(null);
const [viewParams, setViewParams] = useState({
center: null,
radiusKm: 250,
});
const [viewParams, setViewParams] = useState({ center: null, radiusKm: 250 });
const [hasLastView, setHasLastView] = useState(false);
function closeExpandedIfAny() {
const el = expandedElRef.current;
if (el && el.__setExpanded) {
el.__setExpanded(false);
if (el && el.__renderCompact) {
el.__renderCompact();
}
expandedElRef.current = null;
}
@ -76,7 +66,7 @@ export function useMapCore(theme) {
antialias: true,
});
// Reprise dernière vue
// restore last view
let hadLastView = false;
try {
const raw = localStorage.getItem(LAST_VIEW_KEY);
@ -98,7 +88,7 @@ export function useMapCore(theme) {
map.addControl(new maplibregl.NavigationControl(), "top-right");
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 el = expandedElRef.current;
if (el) applyOcclusionForExpanded(markersRef, el);
@ -106,18 +96,8 @@ export function useMapCore(theme) {
map.on("move", 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", () => {
const vp = getViewFromMap(map);
setViewParams(vp);
setViewParams(getViewFromMap(map));
});
map.on("moveend", () => {
@ -127,16 +107,12 @@ export function useMapCore(theme) {
const center = vp.center;
localStorage.setItem(
LAST_VIEW_KEY,
JSON.stringify({
lat: center[1],
lon: center[0],
zoom: map.getZoom(),
})
JSON.stringify({ lat: center[1], lon: center[0], zoom: map.getZoom() })
);
} catch {}
});
// IMPORTANT: click/drag sur la map => fermer le gros post
// ✅ close expanded when clicking/dragging map
map.on("click", () => closeExpandedIfAny());
map.on("dragstart", () => closeExpandedIfAny());
@ -148,6 +124,7 @@ export function useMapCore(theme) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// theme switch
useEffect(() => {
const map = mapRef.current;
if (!map) return;
@ -162,4 +139,4 @@ export function useMapCore(theme) {
viewParams,
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 {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
width: 0;
height: 0;
pointer-events: none; /* children clickable */
transform: translate3d(0, 0, 0);
}
.post-pin * {
pointer-events: auto;
/* IMPORTANT: NE PAS mettre position ici, sinon on casse l'ancrage MapLibre */
/* MapLibre applique position:absolute via .maplibregl-marker */
z-index: 1;
box-sizing: border-box;
}
/* quand expand => au-dessus de TOUT */
.post-pin--expanded {
z-index: 99999;
}
/* ===== ÉTAT COMPACT : petite bulle + petite pointe ===== */
/* =========================
COMPACT
========================= */
.post-pin--compact .post-pin-bubble {
position: absolute;
left: 0;
top: 0;
transform: translate(-50%, calc(-100% - 12px));
display: inline-flex;
align-items: center;
padding: 4px 8px;
@ -25,6 +33,7 @@
background: rgba(6, 40, 80, 0.95);
border: 1px solid rgba(80, 180, 255, 0.9);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
max-width: 180px;
}
.post-pin-dot {
@ -33,32 +42,45 @@
border-radius: 999px;
background: #ff8a00;
margin-right: 6px;
flex-shrink: 0;
}
.post-pin-text {
color: #e8f5ff;
font-size: 11px;
font-weight: 500;
max-width: 120px;
max-width: 160px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* petite pointe */
.post-pin-pointer-small {
/* little pointer tip */
.post-pin--compact .post-pin-pointer-small {
position: absolute;
left: 0;
top: 0;
transform: translate(-50%, -100%);
width: 0;
height: 0;
margin-top: 2px;
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-top: 9px solid rgba(6, 40, 80, 0.95);
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 {
position: absolute;
left: 0;
top: 0;
transform: translate(-50%, calc(-100% - 14px));
min-width: 260px;
max-width: 320px;
background: rgba(8, 20, 40, 0.97);
@ -67,7 +89,6 @@
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.8);
border: 1px solid rgba(90, 190, 255, 0.9);
color: #e8f5ff;
box-sizing: border-box;
}
/* header : catégorie + heure/date */
@ -96,7 +117,6 @@
margin-bottom: 6px;
}
/* fake bloc image/vidéo */
.post-card-media {
width: 72px;
height: 72px;
@ -105,6 +125,7 @@
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.post-card-media-label {
@ -113,9 +134,9 @@
color: #e8f5ff;
}
/* texte principal */
.post-card-info {
flex: 1;
min-width: 0;
}
.post-card-title {
@ -142,6 +163,7 @@
display: flex;
gap: 6px;
margin-bottom: 6px;
flex-wrap: wrap;
}
.post-card-btn {
@ -154,7 +176,6 @@
cursor: pointer;
}
/* zone commentaires */
.post-card-comments {
font-size: 10px;
color: #9eb7ff;
@ -163,13 +184,16 @@
background: rgba(3, 14, 32, 0.95);
}
/* Pointe sous le gros post */
.post-card-pointer {
/* pointer under expanded card */
.post-pin--expanded .post-card-pointer {
position: absolute;
left: 0;
top: 0;
transform: translate(-50%, -100%);
width: 0;
height: 0;
margin-top: 3px;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 11px solid rgba(8, 20, 40, 0.97);
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.8));
}
}