pewsqie pardair
This commit is contained in:
parent
68c3648768
commit
87394a44eb
|
|
@ -7,69 +7,17 @@ import { cardTokens } from "../../theme/cardTokens";
|
|||
import { getTemplateSpecForPost, adaptPostToTemplateData } from "./templateSpecs";
|
||||
|
||||
export function clearAllMarkers(markersRef, expandedElRef) {
|
||||
// close overlay if any
|
||||
try {
|
||||
const el = expandedElRef?.current;
|
||||
if (el && el.__swClose) el.__swClose();
|
||||
} catch {}
|
||||
|
||||
if (markersRef.current) {
|
||||
for (const m of markersRef.current) {
|
||||
try {
|
||||
if (m?.el && m.el.__swUnmount) m.el.__swUnmount();
|
||||
} catch {}
|
||||
try {
|
||||
m.marker.remove();
|
||||
} catch {}
|
||||
try { if (m?.el && m.el.__swUnmount) m.el.__swUnmount(); } catch {}
|
||||
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) continue;
|
||||
|
||||
const r = el.getBoundingClientRect();
|
||||
|
||||
// Only hide markers that are below/behind the expanded card
|
||||
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 applyOcclusionForExpanded() {}
|
||||
export function clearOcclusion(markersRef) {
|
||||
const list = markersRef.current || [];
|
||||
for (const item of list) {
|
||||
|
|
@ -91,9 +39,7 @@ function mountCard(container, spec, data, theme) {
|
|||
themeTokens: tokens,
|
||||
onAction: (action, value) => {
|
||||
if (action?.type === "open_url" && value) {
|
||||
try {
|
||||
window.open(value, "_blank");
|
||||
} catch {}
|
||||
try { window.open(value, "_blank"); } catch {}
|
||||
}
|
||||
},
|
||||
className: "",
|
||||
|
|
@ -101,9 +47,7 @@ function mountCard(container, spec, data, theme) {
|
|||
);
|
||||
|
||||
return () => {
|
||||
try {
|
||||
root.unmount();
|
||||
} catch {}
|
||||
try { root.unmount(); } catch {}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -115,164 +59,193 @@ function escapeHtml(str) {
|
|||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overlay: fixed card at the bottom + pointer that follows the marker.
|
||||
* We only pan the minimum if the marker ends up behind the card.
|
||||
*/
|
||||
function getOrCreateExpandedOverlay(map) {
|
||||
const container = map.getContainer();
|
||||
if (!container) return null;
|
||||
function normCatLabel(post) {
|
||||
const c = (post?.category || "").toString().toUpperCase();
|
||||
if (c === "NEWS") return "NEWS";
|
||||
if (c === "EVENT" || c === "EVENTS") return "EVENTS";
|
||||
if (c === "FRIENDS") return "FRIENDS";
|
||||
if (c === "MARKET") return "MARKET";
|
||||
return "NEWS";
|
||||
}
|
||||
|
||||
let root = container.querySelector(".sw-expanded-overlay-root");
|
||||
if (root) return root;
|
||||
function formatRelativeTime(createdAt) {
|
||||
try {
|
||||
if (!createdAt) return "now";
|
||||
const d = createdAt instanceof Date ? createdAt : new Date(String(createdAt));
|
||||
if (Number.isNaN(d.getTime())) return "now";
|
||||
|
||||
root = document.createElement("div");
|
||||
root.className = "sw-expanded-overlay-root";
|
||||
const s = Math.floor((Date.now() - d.getTime()) / 1000);
|
||||
if (s < 60) return "now";
|
||||
const m = Math.floor(s / 60);
|
||||
if (m < 60) return `${m}m`;
|
||||
const h = Math.floor(m / 60);
|
||||
if (h < 48) return `${h}h`;
|
||||
const days = Math.floor(h / 24);
|
||||
return `${days}d`;
|
||||
} catch {
|
||||
return "now";
|
||||
}
|
||||
}
|
||||
|
||||
root.innerHTML = `
|
||||
<div class="sw-expanded-overlay-card">
|
||||
<div class="post-card sw-expanded-shell">
|
||||
<div class="sw-expanded-top">
|
||||
<div class="sw-expanded-left">
|
||||
<div class="sw-template-full-wrap"></div>
|
||||
</div>
|
||||
<div class="sw-expanded-right">
|
||||
<div class="sw-watch-title">Watching</div>
|
||||
<div class="sw-watch-list">
|
||||
<div class="sw-watch-item">☑ Julia ★<div class="sw-watch-msg">— I see it…</div></div>
|
||||
<div class="sw-watch-item">☑ Kim / CNN<div class="sw-watch-msg">— Blah blah blah</div></div>
|
||||
<div class="sw-watch-item">☑ Yan ★<div class="sw-watch-msg">— Watch this!!</div></div>
|
||||
</div>
|
||||
<button class="sw-add-feed-btn" type="button">ADD TO FEED</button>
|
||||
function closeCenteredOverlay(map) {
|
||||
try {
|
||||
const ov = map?.__swCenteredOverlay;
|
||||
if (!ov) return;
|
||||
|
||||
if (ov.onKey) window.removeEventListener("keydown", ov.onKey);
|
||||
try { if (ov.unmountFull) ov.unmountFull(); } catch {}
|
||||
try { if (ov.unmountMini) ov.unmountMini(); } catch {}
|
||||
try { ov.backdrop?.remove?.(); } catch {}
|
||||
|
||||
map.__swCenteredOverlay = null;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function openCenteredOverlay({ map, post, theme }) {
|
||||
if (!map) return;
|
||||
|
||||
closeCenteredOverlay(map);
|
||||
|
||||
const data = adaptPostToTemplateData(post);
|
||||
const cat = normCatLabel(post);
|
||||
const relTime = formatRelativeTime(post?.created_at || post?.CreatedAt || post?.createdAt);
|
||||
const author = (post?.author || post?.Author || "").toString().trim();
|
||||
const sub = (post?.sub_category || post?.subCategory || "").toString().trim();
|
||||
const metaLeft = author ? `by ${author}` : "";
|
||||
const metaRight = sub ? sub : "";
|
||||
const img = data?.image || "";
|
||||
const url = data?.url || "";
|
||||
|
||||
// Backdrop catches outside click to close
|
||||
const backdrop = document.createElement("div");
|
||||
backdrop.style.position = "fixed";
|
||||
backdrop.style.inset = "0";
|
||||
backdrop.style.zIndex = "9999999";
|
||||
backdrop.style.background = "rgba(0,0,0,0.08)";
|
||||
backdrop.style.backdropFilter = "blur(1px)";
|
||||
backdrop.style.display = "flex";
|
||||
backdrop.style.alignItems = "center";
|
||||
backdrop.style.justifyContent = "center";
|
||||
backdrop.style.padding = "12px";
|
||||
backdrop.style.pointerEvents = "auto";
|
||||
|
||||
// Wrapper uses existing styling hooks
|
||||
const modalWrap = document.createElement("div");
|
||||
modalWrap.className = "post-pin--expanded";
|
||||
modalWrap.style.pointerEvents = "auto";
|
||||
|
||||
// Keep size behavior from your CSS (max-width/min(92vw,...), max-height etc.)
|
||||
modalWrap.innerHTML = `
|
||||
<div class="post-card sw-expanded-shell sw-modal">
|
||||
<div class="sw-modal-toprow">
|
||||
<div class="sw-modal-left">
|
||||
<div class="sw-modal-badge">${escapeHtml(cat)}</div>
|
||||
<div class="sw-modal-meta">
|
||||
${metaLeft ? `<span>${escapeHtml(metaLeft)}</span>` : ""}
|
||||
${metaRight ? `<span>• ${escapeHtml(metaRight)}</span>` : ""}
|
||||
<span>• ${escapeHtml(relTime)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sw-news-generated">
|
||||
<div class="sw-news-title"></div>
|
||||
<div class="sw-news-sub">The news here (generated)</div>
|
||||
</div>
|
||||
<button class="sw-modal-x" type="button" aria-label="Close">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="post-card-footer sw-actions-row">
|
||||
<button class="post-card-btn">Contact</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="sw-modal-hero">
|
||||
${
|
||||
img
|
||||
? `<img src="${escapeHtml(img)}" alt="" loading="lazy" />`
|
||||
: `<div class="sw-modal-hero-fallback"></div>`
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="post-card-comments sw-livechat">
|
||||
<div class="sw-livechat-title">Live chat / comments</div>
|
||||
<div class="sw-livechat-body">
|
||||
<div class="sw-chat-line">— …</div>
|
||||
<div class="sw-chat-line">— what the f***?</div>
|
||||
<div class="sw-chat-line">— nice…</div>
|
||||
<div class="sw-chat-line">— my eyes!!</div>
|
||||
</div>
|
||||
<div class="sw-livechat-inputrow">
|
||||
<input class="sw-livechat-input" placeholder="Write a comment…" />
|
||||
<button class="sw-livechat-post" type="button">POST</button>
|
||||
</div>
|
||||
<div class="sw-modal-title">${escapeHtml(data?.headline || post?.title || "Untitled")}</div>
|
||||
|
||||
<div class="sw-modal-summary">
|
||||
${escapeHtml(data?.summary || post?.snippet || "")}
|
||||
</div>
|
||||
|
||||
<div class="sw-stat-row">
|
||||
<div class="sw-stat-pill">👁 12.4k</div>
|
||||
<div class="sw-stat-pill">❤️ 1.3k</div>
|
||||
<div class="sw-stat-pill">💬 248</div>
|
||||
<div class="sw-stat-pill">📍 ${escapeHtml(post?.city || post?.location_name || "Nearby")}</div>
|
||||
</div>
|
||||
|
||||
<div class="sw-modal-cta-row">
|
||||
<button class="sw-cta-primary" type="button" ${url ? "" : "disabled"} data-url="${escapeHtml(url)}">
|
||||
Open source
|
||||
</button>
|
||||
|
||||
<div class="sw-cta-actions">
|
||||
<button class="post-card-btn" type="button">Contact</button>
|
||||
<button class="post-card-btn" type="button">Like</button>
|
||||
<button class="post-card-btn" type="button">Share</button>
|
||||
<button class="post-card-btn" type="button">Fix</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sw-overlay-line"></div>
|
||||
<div class="sw-overlay-pointer"></div>
|
||||
<div class="post-card-comments sw-livechat">
|
||||
<div class="sw-livechat-title">Live chat / comments</div>
|
||||
<div class="sw-livechat-body">
|
||||
<div class="sw-chat-line">— …</div>
|
||||
<div class="sw-chat-line">— what the f***?</div>
|
||||
<div class="sw-chat-line">— nice…</div>
|
||||
<div class="sw-chat-line">— my eyes!!</div>
|
||||
</div>
|
||||
<div class="sw-livechat-inputrow">
|
||||
<input class="sw-livechat-input" placeholder="Write a comment…" />
|
||||
<button class="sw-livechat-post" type="button">POST</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- (Optional) keep template renderer mounted but hidden for future -->
|
||||
<div class="sw-template-full-wrap" style="display:none;"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// helper close fn
|
||||
root.__swClose = () => {
|
||||
try {
|
||||
if (root.__swUnmountFull) root.__swUnmountFull();
|
||||
} catch {}
|
||||
try {
|
||||
if (root.__swMoveHandler && root.__swMap) {
|
||||
root.__swMap.off("move", root.__swMoveHandler);
|
||||
root.__swMap.off("zoom", root.__swMoveHandler);
|
||||
}
|
||||
} catch {}
|
||||
try {
|
||||
root.remove();
|
||||
} catch {}
|
||||
backdrop.appendChild(modalWrap);
|
||||
document.body.appendChild(backdrop);
|
||||
|
||||
// outside click closes
|
||||
backdrop.addEventListener("click", () => closeCenteredOverlay(map));
|
||||
// inside click doesn't close
|
||||
modalWrap.addEventListener("click", (e) => e.stopPropagation());
|
||||
|
||||
// close button
|
||||
const xbtn = modalWrap.querySelector(".sw-modal-x");
|
||||
if (xbtn) xbtn.addEventListener("click", () => closeCenteredOverlay(map));
|
||||
|
||||
// ESC closes
|
||||
const onKey = (e) => {
|
||||
if (e.key === "Escape") closeCenteredOverlay(map);
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
|
||||
container.appendChild(root);
|
||||
return root;
|
||||
}
|
||||
// CTA open url
|
||||
const cta = modalWrap.querySelector(".sw-cta-primary");
|
||||
if (cta) {
|
||||
cta.addEventListener("click", () => {
|
||||
const u = cta.getAttribute("data-url") || "";
|
||||
if (!u) return;
|
||||
try { window.open(u, "_blank", "noopener,noreferrer"); } catch {}
|
||||
});
|
||||
}
|
||||
|
||||
function updateOverlayPointer(map, overlayRoot, lngLat) {
|
||||
if (!map || !overlayRoot || !lngLat) return;
|
||||
// still mount template full (hidden) so you can switch back later instantly
|
||||
let unmountFull = null;
|
||||
const fullWrap = modalWrap.querySelector(".sw-template-full-wrap");
|
||||
if (fullWrap) {
|
||||
const spec = getTemplateSpecForPost(post, "full");
|
||||
unmountFull = mountCard(fullWrap, spec, data, theme);
|
||||
}
|
||||
|
||||
const p = map.project(lngLat);
|
||||
const pointer = overlayRoot.querySelector(".sw-overlay-pointer");
|
||||
const line = overlayRoot.querySelector(".sw-overlay-line");
|
||||
const cardWrap = overlayRoot.querySelector(".sw-expanded-overlay-card");
|
||||
const card = overlayRoot.querySelector(".sw-expanded-overlay-card .post-card");
|
||||
|
||||
if (!pointer || !line || !cardWrap || !card) return;
|
||||
|
||||
// pointer follows marker
|
||||
pointer.style.left = `${p.x}px`;
|
||||
pointer.style.top = `${p.y}px`;
|
||||
|
||||
// simple line from marker to bottom center of the card
|
||||
const contRect = map.getContainer().getBoundingClientRect();
|
||||
const cardRect = cardWrap.getBoundingClientRect();
|
||||
|
||||
const x1 = p.x;
|
||||
const y1 = p.y;
|
||||
|
||||
const x2 = (cardRect.left - contRect.left) + cardRect.width / 2;
|
||||
const y2 = (cardRect.top - contRect.top) + cardRect.height; // bottom of card
|
||||
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
const len = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// place a thin line
|
||||
line.style.left = `${x1}px`;
|
||||
line.style.top = `${y1}px`;
|
||||
line.style.width = `${len}px`;
|
||||
line.style.transform = `rotate(${Math.atan2(dy, dx)}rad)`;
|
||||
}
|
||||
|
||||
function minimalPanToKeepMarkerVisible(map, overlayRoot, lngLat) {
|
||||
if (!map || !overlayRoot || !lngLat) return;
|
||||
|
||||
const p = map.project(lngLat);
|
||||
const cont = map.getContainer();
|
||||
if (!cont) return;
|
||||
|
||||
const contRect = cont.getBoundingClientRect();
|
||||
const cardWrap = overlayRoot.querySelector(".sw-expanded-overlay-card");
|
||||
if (!cardWrap) return;
|
||||
|
||||
const cardRect = cardWrap.getBoundingClientRect();
|
||||
|
||||
const topPad = 18;
|
||||
const bottomSafe = (cardRect.top - contRect.top) - 18;
|
||||
|
||||
let dy = 0;
|
||||
|
||||
// marker too high -> bring it down a bit
|
||||
if (p.y < topPad) dy = (topPad - p.y);
|
||||
|
||||
// marker behind the card -> bring it up a bit
|
||||
if (p.y > bottomSafe) dy = (bottomSafe - p.y);
|
||||
|
||||
// clamp so it never "vent dalle trop loin"
|
||||
dy = Math.max(-160, Math.min(160, dy));
|
||||
|
||||
if (Math.abs(dy) < 2) return;
|
||||
|
||||
try {
|
||||
const until = Date.now() + 2000;
|
||||
map.__swIgnoreMapClickUntil = until;
|
||||
map.__swIgnoreCloseUntil = until;
|
||||
map.__swIgnoreFetchUntil = until;
|
||||
map.__swIgnoreRebuildUntil = until;
|
||||
} catch {}
|
||||
|
||||
map.panBy([0, dy], { duration: 350 });
|
||||
map.__swCenteredOverlay = {
|
||||
postId: post?.id,
|
||||
backdrop,
|
||||
onKey,
|
||||
unmountFull,
|
||||
unmountMini: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, theme = "blue") {
|
||||
|
|
@ -293,10 +266,7 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the
|
|||
? post.lng
|
||||
: null;
|
||||
|
||||
if (lat == null || lon == null) {
|
||||
console.warn("post sans coordonnée:", post);
|
||||
return;
|
||||
}
|
||||
if (lat == null || lon == null) return;
|
||||
|
||||
const lngLat = [lon, lat];
|
||||
|
||||
|
|
@ -306,9 +276,7 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the
|
|||
|
||||
function unmountIfAny() {
|
||||
if (root.__swUnmount) {
|
||||
try {
|
||||
root.__swUnmount();
|
||||
} catch {}
|
||||
try { root.__swUnmount(); } catch {}
|
||||
root.__swUnmount = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -333,88 +301,6 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the
|
|||
clearOcclusion(markersRef);
|
||||
}
|
||||
|
||||
function closeExpandedOverlayIfAny() {
|
||||
const cur = expandedElRef.current;
|
||||
if (cur && cur.__swClose) {
|
||||
try {
|
||||
cur.__swClose();
|
||||
} catch {}
|
||||
}
|
||||
expandedElRef.current = null;
|
||||
clearOcclusion(markersRef);
|
||||
}
|
||||
|
||||
function renderExpandedOverlay() {
|
||||
// locks so fetch/rebuild doesn't close while opening
|
||||
try {
|
||||
const until = Date.now() + 2000;
|
||||
map.__swIgnoreMapClickUntil = until;
|
||||
map.__swIgnoreCloseUntil = until;
|
||||
map.__swIgnoreFetchUntil = until;
|
||||
map.__swIgnoreRebuildUntil = until;
|
||||
} catch {}
|
||||
|
||||
closeExpandedOverlayIfAny();
|
||||
|
||||
const overlayRoot = getOrCreateExpandedOverlay(map);
|
||||
if (!overlayRoot) return;
|
||||
|
||||
// fill headline
|
||||
const headline = escapeHtml(post?.title || "Untitled");
|
||||
const titleEl = overlayRoot.querySelector(".sw-news-title");
|
||||
if (titleEl) titleEl.innerHTML = headline;
|
||||
|
||||
// mount template full into the left area
|
||||
try {
|
||||
if (overlayRoot.__swUnmountFull) overlayRoot.__swUnmountFull();
|
||||
} catch {}
|
||||
overlayRoot.__swUnmountFull = null;
|
||||
|
||||
const wrap = overlayRoot.querySelector(".sw-template-full-wrap");
|
||||
if (wrap) {
|
||||
wrap.innerHTML = ""; // ensure clean
|
||||
const spec = getTemplateSpecForPost(post, "full");
|
||||
const data = adaptPostToTemplateData(post);
|
||||
overlayRoot.__swUnmountFull = mountCard(wrap, spec, data, theme);
|
||||
}
|
||||
|
||||
// pointer updater
|
||||
const moveHandler = () => {
|
||||
try {
|
||||
updateOverlayPointer(map, overlayRoot, lngLat);
|
||||
applyOcclusionForExpanded(markersRef, overlayRoot);
|
||||
} catch {}
|
||||
};
|
||||
|
||||
overlayRoot.__swMap = map;
|
||||
overlayRoot.__swMoveHandler = moveHandler;
|
||||
|
||||
map.on("move", moveHandler);
|
||||
map.on("zoom", moveHandler);
|
||||
|
||||
// initial layout after paint
|
||||
requestAnimationFrame(() => {
|
||||
try {
|
||||
// 1) update pointer/line
|
||||
updateOverlayPointer(map, overlayRoot, lngLat);
|
||||
|
||||
// 2) minimal pan so marker isn't hidden behind the fixed card
|
||||
minimalPanToKeepMarkerVisible(map, overlayRoot, lngLat);
|
||||
|
||||
// 3) occlusion now + after moveend
|
||||
applyOcclusionForExpanded(markersRef, overlayRoot);
|
||||
map.once("moveend", () => {
|
||||
try {
|
||||
updateOverlayPointer(map, overlayRoot, lngLat);
|
||||
applyOcclusionForExpanded(markersRef, overlayRoot);
|
||||
} catch {}
|
||||
});
|
||||
} catch {}
|
||||
});
|
||||
|
||||
expandedElRef.current = overlayRoot;
|
||||
}
|
||||
|
||||
renderCompact();
|
||||
root.__renderCompact = renderCompact;
|
||||
|
||||
|
|
@ -427,17 +313,13 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the
|
|||
root.addEventListener("click", (ev) => {
|
||||
ev.stopPropagation();
|
||||
|
||||
// toggle: if overlay already open for this post, close it
|
||||
const cur = expandedElRef.current;
|
||||
if (cur && cur.__swForId && cur.__swForId === (post.id ?? `${lon},${lat}`)) {
|
||||
closeExpandedOverlayIfAny();
|
||||
const existing = map.__swCenteredOverlay;
|
||||
if (existing && existing.postId === post?.id) {
|
||||
closeCenteredOverlay(map);
|
||||
return;
|
||||
}
|
||||
|
||||
// open overlay
|
||||
renderExpandedOverlay();
|
||||
if (expandedElRef.current) {
|
||||
expandedElRef.current.__swForId = post.id ?? `${lon},${lat}`;
|
||||
}
|
||||
openCenteredOverlay({ map, post, theme });
|
||||
if (expandedElRef) expandedElRef.current = null;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
export const TEMPLATE_SPECS = {
|
||||
news: {
|
||||
mini_spec: {
|
||||
size: { w: 240, h: 96 }, /* ✅ smaller to avoid layout push */
|
||||
size: { w: 240, h: 96 },
|
||||
background: { type: "gradient", value: "newsBlue" },
|
||||
radius: 18,
|
||||
layers: [
|
||||
|
|
@ -16,23 +16,16 @@ export const TEMPLATE_SPECS = {
|
|||
{ id: "meta", type: "text", x: 12, y: 76, w: 216, h: 16, bind: "data.source", style: "text.meta", maxLines: 1 }
|
||||
],
|
||||
},
|
||||
|
||||
// ✅ FULL: remove useless empty top (no hero image block), keep SAME overall size
|
||||
full_spec: {
|
||||
size: { w: 360, h: 520 },
|
||||
size: { w: 360, h: 260 },
|
||||
background: { type: "solid", value: "surface" },
|
||||
radius: 22,
|
||||
layers: [
|
||||
{ id: "hero", type: "image", x: 0, y: 0, w: 360, h: 220, bind: "data.image" },
|
||||
{ id: "badge", type: "chip", x: 16, y: 16, text: "NEWS", style: "chip.news" },
|
||||
{ id: "title", type: "text", x: 16, y: 240, w: 328, h: 78, bind: "data.headline", style: "text.h1", maxLines: 3 },
|
||||
{ id: "summary", type: "text", x: 16, y: 325, w: 328, h: 110, bind: "data.summary", style: "text.body", maxLines: 5 },
|
||||
{
|
||||
id: "cta",
|
||||
type: "button",
|
||||
x: 16, y: 450, w: 328, h: 48,
|
||||
text: "Open source",
|
||||
style: "button.primary",
|
||||
action: { type: "open_url", bind: "data.url" }
|
||||
}
|
||||
{ id: "badge", type: "chip", x: 16, y: 12, text: "NEWS", style: "chip.news" },
|
||||
{ id: "title", type: "text", x: 16, y: 52, w: 328, h: 72, bind: "data.headline", style: "text.h1", maxLines: 3 },
|
||||
{ id: "summary", type: "text", x: 16, y: 132, w: 328, h: 78, bind: "data.summary", style: "text.body", maxLines: 4 }
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -105,21 +105,9 @@ export function useMapCore(theme) {
|
|||
map.on("zoom", reOcclude);
|
||||
|
||||
// ignore close while auto actions are happening
|
||||
map.on("click", () => {
|
||||
try {
|
||||
const until = map.__swIgnoreMapClickUntil || 0;
|
||||
if (until && Date.now() < until) return;
|
||||
} catch {}
|
||||
closeExpandedIfAny();
|
||||
});
|
||||
map.on("click", () => { closeExpandedIfAny(); });
|
||||
|
||||
map.on("dragstart", () => {
|
||||
try {
|
||||
const until = map.__swIgnoreCloseUntil || 0;
|
||||
if (until && Date.now() < until) return;
|
||||
} catch {}
|
||||
closeExpandedIfAny();
|
||||
});
|
||||
map.on("dragstart", () => { closeExpandedIfAny(); });
|
||||
|
||||
map.on("load", () => setViewParams(getViewFromMap(map)));
|
||||
|
||||
|
|
|
|||
|
|
@ -22,21 +22,13 @@ export function usePostsEngine({
|
|||
const [loadingPosts, setLoadingPosts] = useState(false);
|
||||
const [loadError, setLoadError] = useState("");
|
||||
|
||||
const isExpandedOpen = () => {
|
||||
try {
|
||||
return !!expandedElRef?.current;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const rebuildMarkersForFilters = useCallback(
|
||||
(tf) => {
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
|
||||
// ✅ If expanded overlay/popup is open: NEVER rebuild markers (it closes the expanded)
|
||||
if (isExpandedOpen()) return;
|
||||
// ✅ If expanded popup is open, NEVER rebuild markers (rebuild would close it)
|
||||
if (expandedElRef?.current) return;
|
||||
|
||||
clearAllMarkers(markersRef, expandedElRef);
|
||||
|
||||
|
|
@ -62,7 +54,6 @@ export function usePostsEngine({
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
// ✅ Sociowall list can still update via visiblePosts; markers rebuild is blocked while expanded open.
|
||||
rebuildMarkersForFilters(timeFilter);
|
||||
}, [timeFilter, mainFilter, subFilter, rebuildMarkersForFilters]);
|
||||
|
||||
|
|
@ -74,13 +65,26 @@ export function usePostsEngine({
|
|||
let cancelled = false;
|
||||
|
||||
async function load() {
|
||||
// ✅ If expanded is open: delay fetch indefinitely until user closes it
|
||||
if (isExpandedOpen()) {
|
||||
const mapObj = mapRef.current;
|
||||
|
||||
// ✅ If expanded popup is open: delay fetch (do NOT clear/rebuild while user reads)
|
||||
if (expandedElRef?.current) {
|
||||
if (delayedFetchRef.current) clearTimeout(delayedFetchRef.current);
|
||||
delayedFetchRef.current = setTimeout(load, 1200);
|
||||
delayedFetchRef.current = setTimeout(load, 2500);
|
||||
return;
|
||||
}
|
||||
|
||||
// Also respect ignore window (during auto-pan)
|
||||
try {
|
||||
const until = mapObj?.__swIgnoreFetchUntil || 0;
|
||||
if (until && Date.now() < until) {
|
||||
const wait = Math.min(4000, until - Date.now());
|
||||
if (delayedFetchRef.current) clearTimeout(delayedFetchRef.current);
|
||||
delayedFetchRef.current = setTimeout(load, wait + 25);
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const [lng, lat] = viewParams.center;
|
||||
const radiusKm = viewParams.radiusKm || 750;
|
||||
const filterKey = `${mainFilter}|${subFilter}`;
|
||||
|
|
@ -93,6 +97,7 @@ export function usePostsEngine({
|
|||
|
||||
const lastR = last.radiusKm || 0;
|
||||
const radiusChanged = !lastR || Math.abs(radiusKm - lastR) / Math.max(lastR, 1) >= 0.25;
|
||||
|
||||
const minMoveKm = Math.max(50, radiusKm * 0.25);
|
||||
|
||||
if (!radiusChanged && distKm < minMoveKm) return;
|
||||
|
|
@ -132,9 +137,30 @@ export function usePostsEngine({
|
|||
allPostsRef.current = Array.from(byId.values());
|
||||
lastFetchRef.current = { center: [...viewParams.center], radiusKm, filterKey };
|
||||
|
||||
// ✅ rebuild markers only if expanded is NOT open (double-safety)
|
||||
if (!isExpandedOpen()) rebuildMarkersForFilters(timeFilter);
|
||||
// If expanded popup is open, don't rebuild markers (keeps it open)
|
||||
try {
|
||||
if (expandedElRef?.current) {
|
||||
const posts = allPostsRef.current || [];
|
||||
const catCode2 = categoryCode(mainFilter);
|
||||
|
||||
const visible2 = posts.filter((p) => {
|
||||
if (catCode2) {
|
||||
const pc = (p.category || p.Category || "").toString().toUpperCase();
|
||||
if (pc !== catCode2) return false;
|
||||
}
|
||||
if (!matchesSubFilter(p, subFilter)) return false;
|
||||
if (!matchesTimeFilter(p.created_at || p.CreatedAt, timeFilter)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
setVisiblePosts(visible2);
|
||||
setStatus(visible2.length ? "" : "No posts found.");
|
||||
setLoadingPosts(false);
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
rebuildMarkersForFilters(timeFilter);
|
||||
setLoadingPosts(false);
|
||||
} catch (err) {
|
||||
console.error("Erreur chargement posts:", err);
|
||||
|
|
@ -150,13 +176,11 @@ export function usePostsEngine({
|
|||
return () => {
|
||||
cancelled = true;
|
||||
if (delayedFetchRef.current) {
|
||||
try {
|
||||
clearTimeout(delayedFetchRef.current);
|
||||
} catch {}
|
||||
try { clearTimeout(delayedFetchRef.current); } catch {}
|
||||
delayedFetchRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [viewParams, mainFilter, subFilter, mapRef, rebuildMarkersForFilters, timeFilter]);
|
||||
}, [viewParams, mainFilter, subFilter, mapRef, rebuildMarkersForFilters, timeFilter, expandedElRef]);
|
||||
|
||||
const handleIncomingPost = useCallback(
|
||||
(p) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
:root{
|
||||
--sw-subcats-nudge: -35px; /* subcats vertical nudge */
|
||||
--sw-below-overlap: 40px; /* wall overlaps map a bit */
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -247,3 +247,263 @@
|
|||
display:none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* SW_FIX_FULL_WRAP_COLLAPSE:BEGIN */
|
||||
/* Fix: expanded template wrapper collapsing into a thin line */
|
||||
.post-pin--expanded .post-card.sw-expanded-shell{
|
||||
width: 360px !important;
|
||||
min-width: 360px !important;
|
||||
max-width: 360px !important;
|
||||
height: 520px !important;
|
||||
max-height: 520px !important;
|
||||
overflow: hidden !important; /* no useless scroll */
|
||||
}
|
||||
|
||||
.sw-template-full-wrap{
|
||||
display: block !important;
|
||||
width: 360px !important;
|
||||
height: 520px !important;
|
||||
}
|
||||
|
||||
.sw-template-full-wrap > *{
|
||||
display: block !important;
|
||||
}
|
||||
/* SW_FIX_FULL_WRAP_COLLAPSE:END */
|
||||
|
||||
|
||||
|
||||
/* ===== Expanded header (mock) ===== */
|
||||
.sw-expanded-badge{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
font-weight: 900;
|
||||
font-size: 12px;
|
||||
background: rgba(56,189,248,0.18);
|
||||
color: #D7F3FF;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.sw-expanded-title{
|
||||
margin-top: 10px;
|
||||
font-size: 26px;
|
||||
font-weight: 900;
|
||||
line-height: 1.05;
|
||||
color: #F0F7FF;
|
||||
}
|
||||
|
||||
.sw-expanded-snippet{
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #B5C7E6;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
SW_EXPANDED_POLISH (visual only)
|
||||
- make expanded look like the mini gradients / modern UI
|
||||
========================================================= */
|
||||
.post-pin--expanded .post-card{
|
||||
background: linear-gradient(135deg, rgba(56,189,248,0.16), rgba(7,18,37,0.98) 55%, rgba(2,6,23,0.98));
|
||||
border: 1px solid rgba(96,165,250,0.75);
|
||||
box-shadow: 0 18px 60px rgba(0,0,0,0.55), 0 0 24px rgba(56,189,248,0.22);
|
||||
}
|
||||
|
||||
.sw-expanded-badge{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
padding: 7px 12px;
|
||||
border-radius: 999px;
|
||||
font-weight: 900;
|
||||
font-size: 12px;
|
||||
letter-spacing: .04em;
|
||||
color: #d7f3ff;
|
||||
background: rgba(56,189,248,0.16);
|
||||
border: 1px solid rgba(56,189,248,0.35);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.sw-expanded-title{
|
||||
font-size: 28px;
|
||||
font-weight: 950;
|
||||
line-height: 1.05;
|
||||
color: #f0f7ff;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sw-expanded-meta{
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: rgba(181,199,230,0.95);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.sw-expanded-snippet{
|
||||
font-size: 14px;
|
||||
line-height: 1.35;
|
||||
color: rgba(215,233,255,0.95);
|
||||
background: rgba(3,14,32,0.45);
|
||||
border: 1px solid rgba(90,190,255,0.22);
|
||||
border-radius: 14px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.post-card-btn{
|
||||
background: rgba(56,189,248,0.14);
|
||||
border: 1px solid rgba(56,189,248,0.35);
|
||||
color: #e8f5ff;
|
||||
font-weight: 900;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.post-card-btn:active{
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* ===== CENTER MODAL CONTENT (pretty + consistent) ===== */
|
||||
.sw-modal{
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.sw-modal-toprow{
|
||||
display:flex;
|
||||
align-items:flex-start;
|
||||
justify-content:space-between;
|
||||
gap:10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.sw-modal-badge{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
letter-spacing: .06em;
|
||||
background: rgba(56,189,248,0.16);
|
||||
border: 1px solid rgba(90, 190, 255, 0.28);
|
||||
color:#D7F3FF;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sw-modal-meta{
|
||||
margin-top: 6px;
|
||||
font-size: 11px;
|
||||
color:#9eb7ff;
|
||||
opacity:.95;
|
||||
display:flex;
|
||||
gap:6px;
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
|
||||
.sw-modal-x{
|
||||
border:none;
|
||||
border-radius:999px;
|
||||
width:34px;
|
||||
height:34px;
|
||||
background: rgba(3,14,32,0.55);
|
||||
color:#e8f5ff;
|
||||
font-weight:900;
|
||||
cursor:pointer;
|
||||
border:1px solid rgba(90,190,255,0.25);
|
||||
flex-shrink:0;
|
||||
}
|
||||
|
||||
.sw-modal-hero{
|
||||
width:100%;
|
||||
height: 180px;
|
||||
border-radius: 16px;
|
||||
overflow:hidden;
|
||||
border: 1px solid rgba(90, 190, 255, 0.22);
|
||||
background: radial-gradient(circle at 30% 30%, rgba(56,189,248,0.18), rgba(3,14,32,0.85));
|
||||
box-shadow: 0 10px 24px rgba(0,0,0,0.35);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.sw-modal-hero img{
|
||||
width:100%;
|
||||
height:100%;
|
||||
object-fit: cover;
|
||||
display:block;
|
||||
}
|
||||
|
||||
.sw-modal-hero-fallback{
|
||||
width:100%;
|
||||
height:100%;
|
||||
background: radial-gradient(circle at 35% 35%, rgba(56,189,248,0.22), rgba(3,14,32,0.85));
|
||||
}
|
||||
|
||||
.sw-modal-title{
|
||||
font-size: 22px;
|
||||
font-weight: 900;
|
||||
line-height: 26px;
|
||||
color:#F0F7FF;
|
||||
margin: 6px 0 8px;
|
||||
}
|
||||
|
||||
.sw-modal-summary{
|
||||
font-size: 13px;
|
||||
line-height: 17px;
|
||||
color:#D7E9FF;
|
||||
opacity:.95;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sw-stat-row{
|
||||
display:flex;
|
||||
gap:8px;
|
||||
flex-wrap:wrap;
|
||||
margin: 6px 0 10px;
|
||||
}
|
||||
|
||||
.sw-stat-pill{
|
||||
font-size: 11px;
|
||||
color:#c7e3ff;
|
||||
background: rgba(3, 14, 32, 0.58);
|
||||
border: 1px solid rgba(90, 190, 255, 0.22);
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.sw-modal-cta-row{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.sw-cta-primary{
|
||||
width:100%;
|
||||
border:none;
|
||||
border-radius: 14px;
|
||||
padding: 14px 12px;
|
||||
font-weight: 900;
|
||||
font-size: 14px;
|
||||
color:#e8f5ff;
|
||||
cursor:pointer;
|
||||
background: rgba(56,189,248,0.22);
|
||||
box-shadow: 0 10px 18px rgba(0,0,0,0.35);
|
||||
}
|
||||
|
||||
.sw-cta-primary:disabled{
|
||||
opacity:.55;
|
||||
cursor:default;
|
||||
}
|
||||
|
||||
.sw-cta-actions{
|
||||
display:flex;
|
||||
gap:8px;
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
|
||||
/* mobile tighten */
|
||||
@media (max-width: 640px){
|
||||
.sw-modal-hero{ height: 150px; }
|
||||
.sw-modal-title{ font-size: 20px; line-height: 24px; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,14 +15,9 @@
|
|||
display:flex; flex-direction:column; gap:.5rem;
|
||||
}
|
||||
|
||||
/* ✅ sub-cats: keep above wall overlap + moved DOWN by 10px more */
|
||||
.map-overlay-bottom{
|
||||
bottom: calc(2.6rem + var(--sw-below-overlap, 0px) + env(safe-area-inset-bottom) - 20px);
|
||||
left:50%;
|
||||
transform:translateX(-50%);
|
||||
display:flex;
|
||||
gap:.4rem;
|
||||
}
|
||||
/* ✅ Sub-categories row MUST stay above the Sociowall overlap */
|
||||
/* ✅ DESCEND 25px (was 0 / 10 / 20): */
|
||||
.map-overlay-bottom { bottom: calc(3.2rem + var(--sw-below-overlap, 0px) + env(safe-area-inset-bottom) + var(--sw-subcats-nudge, 0px)); left:50%; transform:translateX(-50%); display:flex; gap:.4rem; }
|
||||
|
||||
.map-overlay-myloc { right:.7rem; bottom:6rem; }
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue