pas pire
This commit is contained in:
parent
1e49157a3a
commit
e52e451d98
|
|
@ -0,0 +1,31 @@
|
||||||
|
const API_BASE = "/api";
|
||||||
|
|
||||||
|
const cache = new Map(); // key -> Promise
|
||||||
|
|
||||||
|
export async function fetchDefaultTemplate(typeKey = "news") {
|
||||||
|
const t = (typeKey || "news").toLowerCase().trim();
|
||||||
|
const res = await fetch(`${API_BASE}/templates/default?type=${encodeURIComponent(t)}`, {
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text().catch(() => "");
|
||||||
|
throw new Error(`fetchDefaultTemplate failed: ${res.status} ${text}`);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// One-per-type cached fetch
|
||||||
|
export function getDefaultTemplateCached(typeKey = "news") {
|
||||||
|
const t = (typeKey || "news").toLowerCase().trim();
|
||||||
|
if (cache.has(t)) return cache.get(t);
|
||||||
|
|
||||||
|
const p = fetchDefaultTemplate(t)
|
||||||
|
.then((tpl) => tpl)
|
||||||
|
.catch((err) => {
|
||||||
|
cache.delete(t);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
cache.set(t, p);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
import React, { useMemo } from "react";
|
||||||
|
import CardRenderer from "../Cards/CardRenderer";
|
||||||
|
import { cardTokens } from "../../theme/cardTokens";
|
||||||
|
|
||||||
|
function normTheme(t) {
|
||||||
|
if (t === "dark" || t === "blue" || t === "light") return t;
|
||||||
|
return "dark";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TemplateMarkerCard({
|
||||||
|
post,
|
||||||
|
template,
|
||||||
|
theme = "dark",
|
||||||
|
mode = "full", // "mini" or "full"
|
||||||
|
onOpenUrl,
|
||||||
|
}) {
|
||||||
|
const spec = mode === "mini" ? template?.mini_spec_json : template?.full_spec_json;
|
||||||
|
|
||||||
|
const data = useMemo(() => {
|
||||||
|
return {
|
||||||
|
headline: post?.title || "Untitled",
|
||||||
|
source: post?.source || (post?.author ? `by ${post.author}` : "") || (post?.sub_category || ""),
|
||||||
|
summary: post?.snippet || post?.body || post?.content || "",
|
||||||
|
url: post?.url || "",
|
||||||
|
image: post?.image_large || post?.image_small || post?.image || post?.media_url || "",
|
||||||
|
};
|
||||||
|
}, [post]);
|
||||||
|
|
||||||
|
if (!spec) {
|
||||||
|
// fallback simple
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
minWidth: 260,
|
||||||
|
maxWidth: 320,
|
||||||
|
background: "rgba(8, 20, 40, 0.97)",
|
||||||
|
borderRadius: 14,
|
||||||
|
padding: "10px 12px",
|
||||||
|
border: "1px solid rgba(90, 190, 255, 0.9)",
|
||||||
|
color: "#e8f5ff",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontWeight: 900, fontSize: 13, marginBottom: 4 }}>{data.headline}</div>
|
||||||
|
<div style={{ opacity: 0.9, fontSize: 11 }}>{data.summary}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = cardTokens[normTheme(theme)] || cardTokens.dark;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardRenderer
|
||||||
|
spec={spec}
|
||||||
|
data={data}
|
||||||
|
themeTokens={tokens}
|
||||||
|
onAction={(action, value) => {
|
||||||
|
if (action?.type === "open_url" && value) {
|
||||||
|
if (onOpenUrl) onOpenUrl(value);
|
||||||
|
else window.open(value, "_blank", "noopener,noreferrer");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className=""
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,18 @@
|
||||||
import maplibregl from "maplibre-gl";
|
import maplibregl from "maplibre-gl";
|
||||||
|
import React from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
import TemplateMarkerCard from "./TemplateMarkerCard";
|
||||||
|
import { getDefaultTemplateCached } from "../../api/templates";
|
||||||
|
|
||||||
export function clearAllMarkers(markersRef, expandedElRef) {
|
export function clearAllMarkers(markersRef, expandedElRef) {
|
||||||
if (markersRef.current) {
|
if (markersRef.current) {
|
||||||
for (const m of markersRef.current) {
|
for (const m of markersRef.current) {
|
||||||
try {
|
try {
|
||||||
|
if (m?.el && m.el.__reactRoot) {
|
||||||
|
try { m.el.__reactRoot.unmount(); } catch {}
|
||||||
|
m.el.__reactRoot = null;
|
||||||
|
}
|
||||||
m.marker.remove();
|
m.marker.remove();
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
@ -63,6 +72,28 @@ export function clearOcclusion(markersRef) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return String(str)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """);
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentTheme() {
|
||||||
|
try {
|
||||||
|
const t = document?.body?.getAttribute("data-theme") || "dark";
|
||||||
|
if (t === "dark" || t === "blue" || t === "light") return t;
|
||||||
|
} catch {}
|
||||||
|
return "dark";
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureReactRoot(el) {
|
||||||
|
if (el.__reactRoot) return el.__reactRoot;
|
||||||
|
el.__reactRoot = createRoot(el);
|
||||||
|
return el.__reactRoot;
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -87,12 +118,6 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = (post.title || "").trim() || "Untitled";
|
const title = (post.title || "").trim() || "Untitled";
|
||||||
const body = (post.snippet || "").trim() || (post.body || "").trim() || title;
|
|
||||||
|
|
||||||
const author = (post.author || "").trim() || "unknown";
|
|
||||||
const created = (post.created_at || "").toString().slice(0, 19);
|
|
||||||
const category = (post.category || "").toUpperCase() || "";
|
|
||||||
const subCat = (post.sub_category || "").toString();
|
|
||||||
|
|
||||||
const root = document.createElement("div");
|
const root = document.createElement("div");
|
||||||
root.className = "post-pin post-pin--compact";
|
root.className = "post-pin post-pin--compact";
|
||||||
|
|
@ -101,6 +126,7 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef) {
|
||||||
function renderCompact() {
|
function renderCompact() {
|
||||||
root.className = "post-pin post-pin--compact";
|
root.className = "post-pin post-pin--compact";
|
||||||
root.style.zIndex = "1";
|
root.style.zIndex = "1";
|
||||||
|
|
||||||
root.innerHTML = `
|
root.innerHTML = `
|
||||||
<div class="post-pin-bubble">
|
<div class="post-pin-bubble">
|
||||||
<div class="post-pin-dot"></div>
|
<div class="post-pin-dot"></div>
|
||||||
|
|
@ -108,48 +134,50 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef) {
|
||||||
</div>
|
</div>
|
||||||
<div class="post-pin-pointer-small"></div>
|
<div class="post-pin-pointer-small"></div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
if (root.__reactRoot) {
|
||||||
|
try { root.__reactRoot.unmount(); } catch {}
|
||||||
|
root.__reactRoot = null;
|
||||||
|
}
|
||||||
|
|
||||||
clearOcclusion(markersRef);
|
clearOcclusion(markersRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderExpanded() {
|
async function renderExpanded() {
|
||||||
root.className = "post-pin post-pin--expanded";
|
root.className = "post-pin post-pin--expanded";
|
||||||
root.style.zIndex = "999999";
|
root.style.zIndex = "999999";
|
||||||
|
|
||||||
root.innerHTML = `
|
root.innerHTML = `
|
||||||
<div class="post-card">
|
<div class="post-card">
|
||||||
<div class="post-card-header">
|
<div class="post-card-body" style="font-size:11px; opacity:.9;">Loading template…</div>
|
||||||
<div class="post-card-cat">
|
|
||||||
${escapeHtml(category || "NEWS")}
|
|
||||||
${subCat ? " · " + escapeHtml(subCat) : ""}
|
|
||||||
</div>
|
|
||||||
<div class="post-card-time">${escapeHtml(created || "")}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="post-card-main">
|
|
||||||
<div class="post-card-media">
|
|
||||||
<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-meta">
|
|
||||||
by ${escapeHtml(author)} · lat ${lat.toFixed(3)} · lon ${lon.toFixed(3)}
|
|
||||||
</div>
|
|
||||||
<div class="post-card-body">${escapeHtml(body)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="post-card-footer">
|
|
||||||
<button class="post-card-btn">Live</button>
|
|
||||||
<button class="post-card-btn">Like</button>
|
|
||||||
<button class="post-card-btn">Share</button>
|
|
||||||
<button class="post-card-btn">Fix</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="post-card-comments">Live chat / comments (placeholder)</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="post-card-pointer"></div>
|
<div class="post-card-pointer"></div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const cardEl = root.querySelector(".post-card");
|
||||||
|
if (!cardEl) return;
|
||||||
|
|
||||||
|
const theme = currentTheme();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tpl = await getDefaultTemplateCached("news");
|
||||||
|
|
||||||
|
const rr = ensureReactRoot(cardEl);
|
||||||
|
rr.render(
|
||||||
|
React.createElement(TemplateMarkerCard, {
|
||||||
|
post,
|
||||||
|
template: tpl,
|
||||||
|
theme,
|
||||||
|
mode: "full",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
cardEl.innerHTML = `
|
||||||
|
<div style="font-weight:800; font-size:13px; margin-bottom:4px;">${escapeHtml(title)}</div>
|
||||||
|
<div style="font-size:11px; opacity:.9;">(template load failed)</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
applyOcclusionForExpanded(markersRef, root);
|
applyOcclusionForExpanded(markersRef, root);
|
||||||
});
|
});
|
||||||
|
|
@ -182,11 +210,3 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(str) {
|
|
||||||
return String(str)
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">")
|
|
||||||
.replace(/"/g, """);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,192 @@
|
||||||
|
import maplibregl from "maplibre-gl";
|
||||||
|
|
||||||
|
export function clearAllMarkers(markersRef, expandedElRef) {
|
||||||
|
if (markersRef.current) {
|
||||||
|
for (const m of markersRef.current) {
|
||||||
|
try {
|
||||||
|
m.marker.remove();
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
markersRef.current = [];
|
||||||
|
if (expandedElRef) expandedElRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rectsOverlap(a, b) {
|
||||||
|
return !(a.right < b.left || a.left > b.right || a.bottom < b.top || a.top > b.bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyOcclusionForExpanded(markersRef, expandedEl) {
|
||||||
|
if (!expandedEl) return;
|
||||||
|
|
||||||
|
const expandedCard = expandedEl.querySelector(".post-card");
|
||||||
|
if (!expandedCard) return;
|
||||||
|
|
||||||
|
const PAD_X = 26;
|
||||||
|
const PAD_TOP = 20;
|
||||||
|
const PAD_BOTTOM = 140;
|
||||||
|
|
||||||
|
const raw = expandedCard.getBoundingClientRect();
|
||||||
|
const expandedRect = {
|
||||||
|
left: raw.left - PAD_X,
|
||||||
|
right: raw.right + PAD_X,
|
||||||
|
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;
|
||||||
|
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearOcclusion(markersRef) {
|
||||||
|
const list = markersRef.current || [];
|
||||||
|
for (const item of list) {
|
||||||
|
const el = item?.el;
|
||||||
|
if (!el) continue;
|
||||||
|
el.style.visibility = "visible";
|
||||||
|
el.style.pointerEvents = "auto";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMarkerForPost(post, mapRef, markersRef, expandedElRef) {
|
||||||
|
const map = mapRef.current;
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
const lat =
|
||||||
|
typeof post.lat === "number"
|
||||||
|
? post.lat
|
||||||
|
: typeof post.latitude === "number"
|
||||||
|
? post.latitude
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const lon =
|
||||||
|
typeof post.lon === "number"
|
||||||
|
? post.lon
|
||||||
|
: typeof post.lng === "number"
|
||||||
|
? post.lng
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (lat == null || lon == null) {
|
||||||
|
console.warn("post sans coordonnée:", post);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = (post.title || "").trim() || "Untitled";
|
||||||
|
const body = (post.snippet || "").trim() || (post.body || "").trim() || title;
|
||||||
|
|
||||||
|
const author = (post.author || "").trim() || "unknown";
|
||||||
|
const created = (post.created_at || "").toString().slice(0, 19);
|
||||||
|
const category = (post.category || "").toUpperCase() || "";
|
||||||
|
const subCat = (post.sub_category || "").toString();
|
||||||
|
|
||||||
|
const root = document.createElement("div");
|
||||||
|
root.className = "post-pin post-pin--compact";
|
||||||
|
root.style.zIndex = "1";
|
||||||
|
|
||||||
|
function renderCompact() {
|
||||||
|
root.className = "post-pin post-pin--compact";
|
||||||
|
root.style.zIndex = "1";
|
||||||
|
root.innerHTML = `
|
||||||
|
<div class="post-pin-bubble">
|
||||||
|
<div class="post-pin-dot"></div>
|
||||||
|
<div class="post-pin-text">${escapeHtml(title)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="post-pin-pointer-small"></div>
|
||||||
|
`;
|
||||||
|
clearOcclusion(markersRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderExpanded() {
|
||||||
|
root.className = "post-pin post-pin--expanded";
|
||||||
|
root.style.zIndex = "999999";
|
||||||
|
root.innerHTML = `
|
||||||
|
<div class="post-card">
|
||||||
|
<div class="post-card-header">
|
||||||
|
<div class="post-card-cat">
|
||||||
|
${escapeHtml(category || "NEWS")}
|
||||||
|
${subCat ? " · " + escapeHtml(subCat) : ""}
|
||||||
|
</div>
|
||||||
|
<div class="post-card-time">${escapeHtml(created || "")}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="post-card-main">
|
||||||
|
<div class="post-card-media">
|
||||||
|
<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-meta">
|
||||||
|
by ${escapeHtml(author)} · lat ${lat.toFixed(3)} · lon ${lon.toFixed(3)}
|
||||||
|
</div>
|
||||||
|
<div class="post-card-body">${escapeHtml(body)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="post-card-footer">
|
||||||
|
<button class="post-card-btn">Live</button>
|
||||||
|
<button class="post-card-btn">Like</button>
|
||||||
|
<button class="post-card-btn">Share</button>
|
||||||
|
<button class="post-card-btn">Fix</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="post-card-comments">Live chat / comments (placeholder)</div>
|
||||||
|
</div>
|
||||||
|
<div class="post-card-pointer"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
applyOcclusionForExpanded(markersRef, root);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCompact();
|
||||||
|
root.__renderCompact = renderCompact;
|
||||||
|
|
||||||
|
const marker = new maplibregl.Marker({ element: root, anchor: "bottom" })
|
||||||
|
.setLngLat([lon, lat])
|
||||||
|
.addTo(map);
|
||||||
|
|
||||||
|
markersRef.current.push({ id: post.id, marker, el: root, post });
|
||||||
|
|
||||||
|
root.addEventListener("click", (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
if (expandedElRef.current && expandedElRef.current !== root) {
|
||||||
|
if (expandedElRef.current.__renderCompact) expandedElRef.current.__renderCompact();
|
||||||
|
expandedElRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExpanded = root.classList.contains("post-pin--expanded");
|
||||||
|
if (isExpanded) {
|
||||||
|
renderCompact();
|
||||||
|
expandedElRef.current = null;
|
||||||
|
} else {
|
||||||
|
renderExpanded();
|
||||||
|
expandedElRef.current = root;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return String(str)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
import maplibregl from "maplibre-gl";
|
||||||
|
import React from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
import TemplateMarkerCard from "./TemplateMarkerCard";
|
||||||
|
import { getDefaultTemplateCached } from "../../api/templates";
|
||||||
|
|
||||||
|
export function clearAllMarkers(markersRef, expandedElRef) {
|
||||||
|
if (markersRef.current) {
|
||||||
|
for (const m of markersRef.current) {
|
||||||
|
try {
|
||||||
|
// unmount react if present
|
||||||
|
if (m?.el && m.el.__reactRoot) {
|
||||||
|
try { m.el.__reactRoot.unmount(); } catch {}
|
||||||
|
m.el.__reactRoot = null;
|
||||||
|
}
|
||||||
|
m.marker.remove();
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
markersRef.current = [];
|
||||||
|
if (expandedElRef) expandedElRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rectsOverlap(a, b) {
|
||||||
|
return !(a.right < b.left || a.left > b.right || a.bottom < b.top || a.top > b.bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyOcclusionForExpanded(markersRef, expandedEl) {
|
||||||
|
if (!expandedEl) return;
|
||||||
|
|
||||||
|
// our expanded DOM contains .post-card
|
||||||
|
const expandedCard = expandedEl.querySelector(".post-card");
|
||||||
|
if (!expandedCard) return;
|
||||||
|
|
||||||
|
const PAD_X = 26;
|
||||||
|
const PAD_TOP = 20;
|
||||||
|
const PAD_BOTTOM = 140;
|
||||||
|
|
||||||
|
const raw = expandedCard.getBoundingClientRect();
|
||||||
|
const expandedRect = {
|
||||||
|
left: raw.left - PAD_X,
|
||||||
|
right: raw.right + PAD_X,
|
||||||
|
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;
|
||||||
|
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearOcclusion(markersRef) {
|
||||||
|
const list = markersRef.current || [];
|
||||||
|
for (const item of list) {
|
||||||
|
const el = item?.el;
|
||||||
|
if (!el) continue;
|
||||||
|
el.style.visibility = "visible";
|
||||||
|
el.style.pointerEvents = "auto";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return String(str)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """);
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentTheme() {
|
||||||
|
try {
|
||||||
|
const t = document?.body?.getAttribute("data-theme") || "dark";
|
||||||
|
if (t === "dark" || t === "blue" || t === "light") return t;
|
||||||
|
} catch {}
|
||||||
|
return "dark";
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureReactRoot(el) {
|
||||||
|
if (el.__reactRoot) return el.__reactRoot;
|
||||||
|
el.__reactRoot = createRoot(el);
|
||||||
|
return el.__reactRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMarkerForPost(post, mapRef, markersRef, expandedElRef) {
|
||||||
|
const map = mapRef.current;
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
const lat =
|
||||||
|
typeof post.lat === "number"
|
||||||
|
? post.lat
|
||||||
|
: typeof post.latitude === "number"
|
||||||
|
? post.latitude
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const lon =
|
||||||
|
typeof post.lon === "number"
|
||||||
|
? post.lon
|
||||||
|
: typeof post.lng === "number"
|
||||||
|
? post.lng
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (lat == null || lon == null) {
|
||||||
|
console.warn("post sans coordonnée:", post);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = (post.title || "").trim() || "Untitled";
|
||||||
|
|
||||||
|
const root = document.createElement("div");
|
||||||
|
root.className = "post-pin post-pin--compact";
|
||||||
|
root.style.zIndex = "1";
|
||||||
|
|
||||||
|
const mount = document.createElement("div");
|
||||||
|
mount.className = "post-react-mount";
|
||||||
|
root.appendChild(mount);
|
||||||
|
|
||||||
|
function renderCompact() {
|
||||||
|
root.className = "post-pin post-pin--compact";
|
||||||
|
root.style.zIndex = "1";
|
||||||
|
|
||||||
|
// compact stays HTML (fast + matches your CSS)
|
||||||
|
root.innerHTML = `
|
||||||
|
<div class="post-pin-bubble">
|
||||||
|
<div class="post-pin-dot"></div>
|
||||||
|
<div class="post-pin-text">${escapeHtml(title)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="post-pin-pointer-small"></div>
|
||||||
|
`;
|
||||||
|
// compact uses no React; clean old root if any
|
||||||
|
if (root.__reactRoot) {
|
||||||
|
try { root.__reactRoot.unmount(); } catch {}
|
||||||
|
root.__reactRoot = null;
|
||||||
|
}
|
||||||
|
clearOcclusion(markersRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderExpanded() {
|
||||||
|
root.className = "post-pin post-pin--expanded";
|
||||||
|
root.style.zIndex = "999999";
|
||||||
|
|
||||||
|
// Create expanded shell that matches existing occlusion CSS selectors
|
||||||
|
root.innerHTML = `
|
||||||
|
<div class="post-card">
|
||||||
|
<div class="post-card-body" style="font-size:11px; opacity:.9;">Loading template…</div>
|
||||||
|
</div>
|
||||||
|
<div class="post-card-pointer"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Render React inside .post-card (replace loading)
|
||||||
|
const cardEl = root.querySelector(".post-card");
|
||||||
|
if (!cardEl) return;
|
||||||
|
|
||||||
|
const theme = currentTheme();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tpl = await getDefaultTemplateCached("news"); // default type for now
|
||||||
|
const rr = ensureReactRoot(cardEl);
|
||||||
|
rr.render(
|
||||||
|
<TemplateMarkerCard
|
||||||
|
post={post}
|
||||||
|
template={tpl}
|
||||||
|
theme={theme}
|
||||||
|
mode="full"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// fallback if template fetch fails
|
||||||
|
cardEl.innerHTML = `
|
||||||
|
<div style="font-weight:800; font-size:13px; margin-bottom:4px;">${escapeHtml(title)}</div>
|
||||||
|
<div style="font-size:11px; opacity:.9;">(template load failed)</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
applyOcclusionForExpanded(markersRef, root);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCompact();
|
||||||
|
root.__renderCompact = renderCompact;
|
||||||
|
|
||||||
|
const marker = new maplibregl.Marker({ element: root, anchor: "bottom" })
|
||||||
|
.setLngLat([lon, lat])
|
||||||
|
.addTo(map);
|
||||||
|
|
||||||
|
markersRef.current.push({ id: post.id, marker, el: root, post });
|
||||||
|
|
||||||
|
root.addEventListener("click", (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
if (expandedElRef.current && expandedElRef.current !== root) {
|
||||||
|
if (expandedElRef.current.__renderCompact) expandedElRef.current.__renderCompact();
|
||||||
|
expandedElRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExpanded = root.classList.contains("post-pin--expanded");
|
||||||
|
if (isExpanded) {
|
||||||
|
renderCompact();
|
||||||
|
expandedElRef.current = null;
|
||||||
|
} else {
|
||||||
|
renderExpanded();
|
||||||
|
expandedElRef.current = root;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue