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 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 {
|
||||
if (m?.el && m.el.__reactRoot) {
|
||||
try { m.el.__reactRoot.unmount(); } catch {}
|
||||
m.el.__reactRoot = null;
|
||||
}
|
||||
m.marker.remove();
|
||||
} 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) {
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
|
|
@ -87,12 +118,6 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef) {
|
|||
}
|
||||
|
||||
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";
|
||||
|
|
@ -101,6 +126,7 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef) {
|
|||
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>
|
||||
|
|
@ -108,48 +134,50 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef) {
|
|||
</div>
|
||||
<div class="post-pin-pointer-small"></div>
|
||||
`;
|
||||
|
||||
if (root.__reactRoot) {
|
||||
try { root.__reactRoot.unmount(); } catch {}
|
||||
root.__reactRoot = null;
|
||||
}
|
||||
|
||||
clearOcclusion(markersRef);
|
||||
}
|
||||
|
||||
function renderExpanded() {
|
||||
async 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 class="post-card-body" style="font-size:11px; opacity:.9;">Loading template…</div>
|
||||
</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(() => {
|
||||
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