diff --git a/src/App.jsx b/src/App.jsx
index ab708fa..2e993b2 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -32,10 +32,12 @@ export default function App() {
setTheme(saved);
document.body.setAttribute("data-theme", saved);
} else {
- document.body.setAttribute("data-theme", "dark");
+ setTheme("blue");
+ document.body.setAttribute("data-theme", "blue");
}
} catch (e) {
- document.body.setAttribute("data-theme", "dark");
+ setTheme("blue");
+ document.body.setAttribute("data-theme", "blue");
}
}, []);
diff --git a/src/components/Map/MapView.jsx b/src/components/Map/MapView.jsx
index e77bd38..6816770 100644
--- a/src/components/Map/MapView.jsx
+++ b/src/components/Map/MapView.jsx
@@ -31,15 +31,29 @@ export default function MapView({
useMapCore(theme);
const [mainFilter, setMainFilter] = useState("All");
- const [timeFilter, setTimeFilter] = useState("RECENT");
+ const TIME_FILTER_KEY = "sociowire:timeFilter";
+ const [timeFilter, setTimeFilter] = useState(() => {
+ try {
+ const v = localStorage.getItem(TIME_FILTER_KEY);
+ const ok = ["NOW","TODAY","RECENT","PAST"].includes(v);
+ return ok ? v : "TODAY";
+ } catch {
+ return "TODAY";
+ }
+ });
const [subFilter, setSubFilter] = useState("All");
+ useEffect(() => {
+ try { localStorage.setItem(TIME_FILTER_KEY, timeFilter); } catch {}
+ }, [timeFilter]);
+
const stageRef = useRef(null);
const [showSubcats, setShowSubcats] = useState(true);
const userPosition = useUserPosition(mapRef, hasLastView);
const { status, visiblePosts, loadingPosts, loadError, handleIncomingPost } = usePostsEngine({
+ theme,
mapRef,
viewParams,
mainFilter,
diff --git a/src/components/Map/MapView.jsx.bak b/src/components/Map/MapView.jsx.bak
new file mode 100644
index 0000000..da4bebe
--- /dev/null
+++ b/src/components/Map/MapView.jsx.bak
@@ -0,0 +1,423 @@
+import React, { useEffect, useRef, useState } from "react";
+import "maplibre-gl/dist/maplibre-gl.css";
+
+import "../../styles/mapMarkers.css";
+import "../../styles/overlays.css";
+import "../../styles/posts.css";
+import "../../styles/filters.css";
+
+import { createPost } from "../../api/client";
+import { FILTER_MAIN_CATEGORIES, FILTER_CATEGORY_MAP, CREATE_CATEGORY_MAP } from "./mapConfig";
+import { useMapCore } from "./useMapCore";
+import { useUserPosition } from "./useUserPosition";
+import { usePostsEngine } from "./usePostsEngine";
+import { useAuth } from "../../auth/AuthContext";
+import FilterButtons from "../Filters/FilterButtons";
+import TimeFilterButtons from "../Filters/TimeFilterButtons";
+
+export default function MapView({
+ theme = "dark",
+
+ // lift posts to App (so Sociowall is NOT inside the map)
+ onPostsState,
+
+ // selection controlled by App
+ selectedPost,
+ onSelectPost,
+}) {
+ const { authenticated, username, token } = useAuth();
+
+ const { containerRef, mapRef, markersRef, expandedElRef, viewParams, hasLastView } =
+ useMapCore(theme);
+
+ const [mainFilter, setMainFilter] = useState("All");
+ const TIME_FILTER_KEY = "sociowire:timeFilter";
+ const [timeFilter, setTimeFilter] = useState(() => {
+ try {
+ const v = localStorage.getItem(TIME_FILTER_KEY);
+ const ok = ["NOW","TODAY","RECENT","PAST"].includes(v);
+ return ok ? v : "TODAY";
+ } catch {
+ return "TODAY";
+ }
+ });
+ const [subFilter, setSubFilter] = useState("All");
+
+ useEffect(() => {
+ try { localStorage.setItem(TIME_FILTER_KEY, timeFilter); } catch {}
+ }, [timeFilter]);
+
+ const stageRef = useRef(null);
+ const [showSubcats, setShowSubcats] = useState(true);
+
+ const userPosition = useUserPosition(mapRef, hasLastView);
+
+ const { status, visiblePosts, loadingPosts, loadError, handleIncomingPost } = usePostsEngine({
+ mapRef,
+ viewParams,
+ mainFilter,
+ subFilter,
+ timeFilter,
+ markersRef,
+ expandedElRef,
+ });
+
+ const [isCreating, setIsCreating] = useState(false);
+ const [draftTitle, setDraftTitle] = useState("");
+ const [draftBody, setDraftBody] = useState("");
+ const [draftCategory, setDraftCategory] = useState("News");
+ const [draftSubCategory, setDraftSubCategory] = useState(CREATE_CATEGORY_MAP.News[0]);
+ const [draftCoords, setDraftCoords] = useState(null);
+ const [isSaving, setIsSaving] = useState(false);
+ const [saveError, setSaveError] = useState("");
+
+ const bottomCategories = FILTER_CATEGORY_MAP[mainFilter] || ["All"];
+
+ /* SW_SUBCAT_ICONS:BEGIN */
+ const getSubcatIcon = (main, sub) => {
+ const M = {
+ All: {
+ All: "fa-solid fa-layer-group",
+ },
+
+ News: {
+ All: "fa-solid fa-layer-group",
+ World: "fa-solid fa-globe",
+ Politics: "fa-solid fa-landmark-flag",
+ Tech: "fa-solid fa-microchip",
+ Finance: "fa-solid fa-chart-line",
+ Local: "fa-solid fa-location-dot",
+ },
+
+ Friends: {
+ All: "fa-solid fa-layer-group",
+ Nearby: "fa-solid fa-location-crosshairs",
+ Chats: "fa-solid fa-comments",
+ Groups: "fa-solid fa-user-group",
+ Stories: "fa-solid fa-book-open",
+ Favs: "fa-solid fa-heart",
+ },
+
+ Events: {
+ All: "fa-solid fa-layer-group",
+ Today: "fa-solid fa-calendar-day",
+ Weekend: "fa-solid fa-calendar-week",
+ Music: "fa-solid fa-music",
+ Sports: "fa-solid fa-football",
+ Local: "fa-solid fa-location-dot",
+ },
+
+ Market: {
+ All: "fa-solid fa-layer-group",
+ Actu: "fa-solid fa-newspaper",
+ Tech: "fa-solid fa-microchip",
+ Finan: "fa-solid fa-money-bill-trend-up",
+ Sport: "fa-solid fa-basketball",
+ Deals: "fa-solid fa-tags",
+ },
+ };
+
+ return (M[main] && M[main][sub]) || "";
+ };
+ /* SW_SUBCAT_ICONS:END */
+
+ // keep subFilter valid
+ useEffect(() => {
+ if (!bottomCategories.length) return;
+ if (!subFilter || !bottomCategories.includes(subFilter)) setSubFilter("All");
+ }, [mainFilter, bottomCategories, subFilter]);
+
+ // push posts state up to App (for Sociowall)
+ useEffect(() => {
+ if (!onPostsState) return;
+ onPostsState({
+ status,
+ posts: visiblePosts,
+ loading: loadingPosts,
+ error: loadError,
+ });
+ }, [status, visiblePosts, loadingPosts, loadError, onPostsState]);
+
+ // if App selects a post (from Sociowall), fly to it on map
+ useEffect(() => {
+ if (!selectedPost) return;
+ const map = mapRef.current;
+ if (!map) return;
+
+ const lng = selectedPost.lon ?? selectedPost.lng;
+ const lat = selectedPost.lat ?? selectedPost.latitude;
+
+ if (typeof lng === "number" && typeof lat === "number") {
+ map.flyTo({ center: [lng, lat], zoom: 8, speed: 1.4 });
+ }
+ }, [selectedPost, mapRef]);
+
+ // Websocket for new posts
+ useEffect(() => {
+ const el = stageRef.current;
+ if (!el || typeof IntersectionObserver === "undefined") return;
+
+ const obs = new IntersectionObserver(
+ (entries) => {
+ const e = entries && entries[0];
+ const ok = !!(e && e.isIntersecting && e.intersectionRatio > 0.15);
+ setShowSubcats(ok);
+ },
+ { threshold: [0, 0.15, 0.3, 0.6] }
+ );
+
+ obs.observe(el);
+ return () => obs.disconnect();
+ }, []);
+
+ useEffect(() => {
+ const proto = window.location.protocol === "https:" ? "wss" : "ws";
+ const host =
+ window.location.port === "5173"
+ ? `${window.location.hostname}:8081`
+ : window.location.host;
+ const wsUrl = `${proto}://${host}/ws`;
+
+ let ws;
+ try {
+ ws = new WebSocket(wsUrl);
+ } catch {
+ return;
+ }
+
+ ws.onmessage = (ev) => {
+ try {
+ const msg = JSON.parse(ev.data);
+ if (msg.type === "new_post" && msg.post) handleIncomingPost(msg.post);
+ } catch {}
+ };
+
+ return () => ws && ws.close();
+ }, [handleIncomingPost]);
+
+ const handleFlyToMe = () => {
+ const map = mapRef.current;
+ if (!map || !userPosition) return;
+ map.flyTo({ center: userPosition, zoom: 9, speed: 1.2 });
+ };
+
+ const handleOpenCreate = () => {
+ if (!authenticated) {
+ alert("You must be logged in to place a wire.");
+ return;
+ }
+ setIsCreating(true);
+ setDraftTitle("");
+ setDraftBody("");
+ const main = mainFilter === "All" ? "News" : mainFilter;
+ setDraftCategory(main);
+ const list = CREATE_CATEGORY_MAP[main] || CREATE_CATEGORY_MAP.News;
+ setDraftSubCategory(list[0]);
+ setDraftCoords(null);
+ };
+
+ const handleSearchClick = () => {
+ alert("🔍 Search feature coming soon!");
+ };
+
+ const handleSubmitPost = async () => {
+ if (isSaving) return;
+ setSaveError("");
+
+ const map = mapRef.current;
+ if (!map) return;
+
+ if (!authenticated) {
+ setSaveError("You must be logged in to post.");
+ return;
+ }
+
+ const authorName = (username || "").trim() || "anon";
+
+ let baseLng;
+ let baseLat;
+
+ if (draftCoords && draftCoords.length === 2) {
+ baseLng = draftCoords[0];
+ baseLat = draftCoords[1];
+ } else {
+ const center = map.getCenter();
+ baseLng = center.lng;
+ baseLat = center.lat;
+ }
+
+ // keep marker visually above pointer
+ const pixelOffsetY = -20;
+ const screenPoint = map.project([baseLng, baseLat]);
+ const correctedPoint = { x: screenPoint.x, y: screenPoint.y + pixelOffsetY };
+ const correctedLngLat = map.unproject([correctedPoint.x, correctedPoint.y]);
+ const lng = correctedLngLat.lng;
+ const lat = correctedLngLat.lat;
+
+ if (!draftTitle.trim()) {
+ setSaveError("Title required.");
+ return;
+ }
+
+ try {
+ setIsSaving(true);
+
+ const newPost = await createPost(
+ {
+ title: draftTitle.trim(),
+ snippet: draftBody.trim() || draftTitle.trim(),
+ category: draftCategory === "Events" ? "EVENT" : draftCategory.toUpperCase(),
+ sub_category: draftSubCategory || "ALL",
+ lat,
+ lon: lng,
+ author: authorName,
+ },
+ token
+ );
+
+ if (newPost && newPost.id) handleIncomingPost(newPost);
+
+ setIsSaving(false);
+ setIsCreating(false);
+ } catch (e) {
+ console.error(e);
+ setIsSaving(false);
+ setSaveError("Error creating post.");
+ }
+ };
+
+ const handleMainFilterSelect = (code) => {
+ if (!FILTER_MAIN_CATEGORIES.includes(code)) return;
+ setMainFilter(code);
+ };
+
+ // allow selecting from map later (optional hook)
+ const handleSelectPost = (post) => {
+ if (onSelectPost) onSelectPost(post);
+ const map = mapRef.current;
+ if (!map) return;
+ const lng = post.lon ?? post.lng;
+ const lat = post.lat ?? post.latitude;
+ if (typeof lng === "number" && typeof lat === "number") {
+ map.flyTo({ center: [lng, lat], zoom: 8, speed: 1.4 });
+ }
+ };
+
+ return (
+
+
+
+ {status &&
{status}
}
+
+
+
+
+
+
+
+ setTimeFilter(code)} />
+
+
+
+
+
+
+
+
+ {bottomCategories.map((c) => (
+
+ ))}
+
+
+
+
+
+
+
+ {isCreating && (
+
+ )}
+
+ {isCreating && (
+
+
+ Create wire
+
+
+
+
+ Move the map, aim with the crosshair — your wire will be pinned there.
+
+
+
+
+
+
+
+
+
setDraftTitle(e.target.value)}
+ />
+
+
+ )}
+
+
+ );
+}
diff --git a/src/components/Map/mapGeo.js b/src/components/Map/mapGeo.js
index 0e4ccbe..0ed6d66 100644
--- a/src/components/Map/mapGeo.js
+++ b/src/components/Map/mapGeo.js
@@ -24,7 +24,7 @@ export function getViewFromMap(map) {
radiusKm = 750;
}
- radiusKm = Math.min(Math.max(radiusKm, 25), 1500);
+ radiusKm = Math.min(Math.max(radiusKm, 25), 1000);
return {
center: [center.lng, center.lat],
diff --git a/src/components/Map/markerManager.js b/src/components/Map/markerManager.js
index 5158a35..dbbf505 100644
--- a/src/components/Map/markerManager.js
+++ b/src/components/Map/markerManager.js
@@ -2,6 +2,8 @@ import maplibregl from "maplibre-gl";
import React from "react";
import { createRoot } from "react-dom/client";
+const SW_ANIM_MS = 840;
+
import CardRenderer from "../Cards/CardRenderer";
import { cardTokens } from "../../theme/cardTokens";
import { getTemplateSpecForPost, adaptPostToTemplateData } from "./templateSpecs";
@@ -91,11 +93,25 @@ function closeCenteredOverlay(map) {
try {
const ov = map?.__swCenteredOverlay;
if (!ov) return;
+ if (ov.closing) return;
if (ov.onKey) window.removeEventListener("keydown", ov.onKey);
+
+ // animate out
+ try {
+ const mw = ov.modalWrapRef;
+ if (mw) {
+ mw.style.opacity = "0";
+ mw.style.transform = "scale(0.96)";
+ }
+ if (ov.backdrop) ov.backdrop.style.opacity = "0";
+ } catch {}
try { if (ov.unmountFull) ov.unmountFull(); } catch {}
try { if (ov.unmountMini) ov.unmountMini(); } catch {}
- try { ov.backdrop?.remove?.(); } catch {}
+ try {
+ const b = ov.backdrop;
+ if (b) setTimeout(() => { try { b.remove(); } catch {} }, 300);
+ } catch {}
map.__swCenteredOverlay = null;
} catch {}
@@ -118,6 +134,9 @@ function openCenteredOverlay({ map, post, theme }) {
// Backdrop catches outside click to close
const backdrop = document.createElement("div");
+ try { backdrop.classList.add("sw-centered-backdrop","sw-enter"); } catch {}
+ backdrop.classList.add("sw-centered-backdrop");
+ backdrop.className = "sw-centered-backdrop";
backdrop.style.position = "fixed";
backdrop.style.inset = "0";
backdrop.style.zIndex = "9999999";
@@ -131,7 +150,8 @@ function openCenteredOverlay({ map, post, theme }) {
// Wrapper uses existing styling hooks
const modalWrap = document.createElement("div");
- modalWrap.className = "post-pin--expanded";
+ modalWrap.classList.add("sw-centered-modal");
+ modalWrap.className = "post-pin--expanded sw-centered-modal";
modalWrap.style.pointerEvents = "auto";
// Keep size behavior from your CSS (max-width/min(92vw,...), max-height etc.)
@@ -205,6 +225,31 @@ function openCenteredOverlay({ map, post, theme }) {
backdrop.appendChild(modalWrap);
document.body.appendChild(backdrop);
+ requestAnimationFrame(() => {
+ try { backdrop.classList.add("sw-enter-active"); } catch {}
+ try { modalWrap.classList.add("sw-enter-active"); } catch {}
+ });
+
+ // animate in
+ backdrop.style.opacity = "0";
+ modalWrap.style.opacity = "0";
+ modalWrap.style.transform = "scale(0.96)";
+ requestAnimationFrame(() => {
+ backdrop.style.transition = "opacity 260ms ease";
+ modalWrap.style.transition = "opacity 260ms ease, transform 320ms cubic-bezier(.2,.9,.2,1)";
+ backdrop.style.opacity = "1";
+ modalWrap.style.opacity = "1";
+ modalWrap.style.transform = "scale(1)";
+ });
+
+
+ // ✅ animate in (fade + scale)
+ try {
+ requestAnimationFrame(() => {
+ backdrop.classList.add("sw-open");
+ modalWrap.classList.add("sw-open");
+ });
+ } catch {}
// outside click closes
backdrop.addEventListener("click", () => closeCenteredOverlay(map));
@@ -241,6 +286,7 @@ function openCenteredOverlay({ map, post, theme }) {
map.__swCenteredOverlay = {
postId: post?.id,
+ modalWrapRef: modalWrap,
backdrop,
onKey,
unmountFull,
@@ -271,6 +317,8 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the
const lngLat = [lon, lat];
const root = document.createElement("div");
+ root.classList.add("sw-appear");
+ requestAnimationFrame(() => root.classList.add("sw-appear-in"));
root.className = "post-pin post-pin--compact";
root.style.zIndex = "1";
@@ -283,7 +331,7 @@ export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, the
function renderCompact() {
unmountIfAny();
- root.className = "post-pin post-pin--compact";
+ root.className = "post-pin post-pin--compact sw-appear sw-appear-in";
root.style.zIndex = "1";
root.innerHTML = `
diff --git a/src/components/Map/markerManager.js.bak2 b/src/components/Map/markerManager.js.bak2
new file mode 100644
index 0000000..e143acd
--- /dev/null
+++ b/src/components/Map/markerManager.js.bak2
@@ -0,0 +1,366 @@
+import maplibregl from "maplibre-gl";
+import React from "react";
+import { createRoot } from "react-dom/client";
+
+import CardRenderer from "../Cards/CardRenderer";
+import { cardTokens } from "../../theme/cardTokens";
+import { getTemplateSpecForPost, adaptPostToTemplateData } from "./templateSpecs";
+
+export function clearAllMarkers(markersRef, expandedElRef) {
+ 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 {}
+ }
+ }
+ markersRef.current = [];
+ if (expandedElRef) expandedElRef.current = null;
+}
+
+export function applyOcclusionForExpanded() {}
+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 mountCard(container, spec, data, theme) {
+ const root = createRoot(container);
+ const tokens = cardTokens?.[theme] || cardTokens.blue;
+
+ root.render(
+ React.createElement(CardRenderer, {
+ spec,
+ data,
+ themeTokens: tokens,
+ onAction: (action, value) => {
+ if (action?.type === "open_url" && value) {
+ try { window.open(value, "_blank"); } catch {}
+ }
+ },
+ className: "",
+ })
+ );
+
+ return () => {
+ try { root.unmount(); } catch {}
+ };
+}
+
+function escapeHtml(str) {
+ return String(str ?? "")
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+}
+
+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";
+}
+
+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";
+
+ 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";
+ }
+}
+
+function closeCenteredOverlay(map) {
+ try {
+ const ov = map?.__swCenteredOverlay;
+ if (!ov) return;
+ if (ov.closing) return;
+
+ if (ov.onKey) window.removeEventListener("keydown", ov.onKey);
+
+ // animate out
+ try {
+ const mw = ov.modalWrapRef;
+ if (mw) {
+ mw.style.opacity = "0";
+ mw.style.transform = "scale(0.96)";
+ }
+ if (ov.backdrop) ov.backdrop.style.opacity = "0";
+ } catch {}
+ try { if (ov.unmountFull) ov.unmountFull(); } catch {}
+ try { if (ov.unmountMini) ov.unmountMini(); } catch {}
+ try {
+ const b = ov.backdrop;
+ if (b) setTimeout(() => { try { b.remove(); } catch {} }, 300);
+ } 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.classList.add("sw-centered-backdrop");
+ backdrop.className = "sw-centered-backdrop";
+ 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.classList.add("sw-centered-modal");
+ modalWrap.className = "post-pin--expanded sw-centered-modal";
+ modalWrap.style.pointerEvents = "auto";
+
+ // Keep size behavior from your CSS (max-width/min(92vw,...), max-height etc.)
+ modalWrap.innerHTML = `
+
+
+
+
${escapeHtml(cat)}
+
+ ${metaLeft ? `${escapeHtml(metaLeft)}` : ""}
+ ${metaRight ? `• ${escapeHtml(metaRight)}` : ""}
+ • ${escapeHtml(relTime)}
+
+
+
+
+
+
+
+ ${
+ img
+ ? `
})
`
+ : `
`
+ }
+
+
+
${escapeHtml(data?.headline || post?.title || "Untitled")}
+
+
+ ${escapeHtml(data?.summary || post?.snippet || "")}
+
+
+
+
👁 12.4k
+
❤️ 1.3k
+
💬 248
+
📍 ${escapeHtml(post?.city || post?.location_name || "Nearby")}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ backdrop.appendChild(modalWrap);
+ document.body.appendChild(backdrop);
+
+ // animate in
+ backdrop.style.opacity = "0";
+ modalWrap.style.opacity = "0";
+ modalWrap.style.transform = "scale(0.96)";
+ requestAnimationFrame(() => {
+ backdrop.style.transition = "opacity 260ms ease";
+ modalWrap.style.transition = "opacity 260ms ease, transform 320ms cubic-bezier(.2,.9,.2,1)";
+ backdrop.style.opacity = "1";
+ modalWrap.style.opacity = "1";
+ modalWrap.style.transform = "scale(1)";
+ });
+
+
+ // ✅ animate in (fade + scale)
+ try {
+ requestAnimationFrame(() => {
+ backdrop.classList.add("sw-open");
+ modalWrap.classList.add("sw-open");
+ });
+ } catch {}
+
+ // 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);
+
+ // 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 {}
+ });
+ }
+
+ // 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);
+ }
+
+ map.__swCenteredOverlay = {
+ postId: post?.id,
+ modalWrapRef: modalWrap,
+ backdrop,
+ onKey,
+ unmountFull,
+ unmountMini: null,
+ };
+}
+
+export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, theme = "blue") {
+ 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) return;
+
+ const lngLat = [lon, lat];
+
+ const root = document.createElement("div");
+ root.classList.add("sw-appear");
+ requestAnimationFrame(() => root.classList.add("sw-appear-in"));
+ root.className = "post-pin post-pin--compact";
+ root.style.zIndex = "1";
+
+ function unmountIfAny() {
+ if (root.__swUnmount) {
+ try { root.__swUnmount(); } catch {}
+ root.__swUnmount = null;
+ }
+ }
+
+ function renderCompact() {
+ unmountIfAny();
+ root.className = "post-pin post-pin--compact";
+ root.style.zIndex = "1";
+
+ root.innerHTML = `
+
+
+ `;
+
+ const wrap = root.querySelector(".sw-template-mini-wrap");
+ if (wrap) {
+ const spec = getTemplateSpecForPost(post, "mini");
+ const data = adaptPostToTemplateData(post);
+ root.__swUnmount = mountCard(wrap, spec, data, theme);
+ }
+
+ clearOcclusion(markersRef);
+ }
+
+ renderCompact();
+ root.__renderCompact = renderCompact;
+
+ const marker = new maplibregl.Marker({ element: root, anchor: "bottom" })
+ .setLngLat(lngLat)
+ .addTo(map);
+
+ markersRef.current.push({ id: post.id, marker, el: root, post });
+
+ root.addEventListener("click", (ev) => {
+ ev.stopPropagation();
+
+ const existing = map.__swCenteredOverlay;
+ if (existing && existing.postId === post?.id) {
+ closeCenteredOverlay(map);
+ return;
+ }
+
+ openCenteredOverlay({ map, post, theme });
+ if (expandedElRef) expandedElRef.current = null;
+ });
+}
diff --git a/src/components/Map/markerManager.js.bak3 b/src/components/Map/markerManager.js.bak3
new file mode 100644
index 0000000..0c99c29
--- /dev/null
+++ b/src/components/Map/markerManager.js.bak3
@@ -0,0 +1,366 @@
+import maplibregl from "maplibre-gl";
+import React from "react";
+import { createRoot } from "react-dom/client";
+
+import CardRenderer from "../Cards/CardRenderer";
+import { cardTokens } from "../../theme/cardTokens";
+import { getTemplateSpecForPost, adaptPostToTemplateData } from "./templateSpecs";
+
+export function clearAllMarkers(markersRef, expandedElRef) {
+ 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 {}
+ }
+ }
+ markersRef.current = [];
+ if (expandedElRef) expandedElRef.current = null;
+}
+
+export function applyOcclusionForExpanded() {}
+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 mountCard(container, spec, data, theme) {
+ const root = createRoot(container);
+ const tokens = cardTokens?.[theme] || cardTokens.blue;
+
+ root.render(
+ React.createElement(CardRenderer, {
+ spec,
+ data,
+ themeTokens: tokens,
+ onAction: (action, value) => {
+ if (action?.type === "open_url" && value) {
+ try { window.open(value, "_blank"); } catch {}
+ }
+ },
+ className: "",
+ })
+ );
+
+ return () => {
+ try { root.unmount(); } catch {}
+ };
+}
+
+function escapeHtml(str) {
+ return String(str ?? "")
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+}
+
+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";
+}
+
+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";
+
+ 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";
+ }
+}
+
+function closeCenteredOverlay(map) {
+ try {
+ const ov = map?.__swCenteredOverlay;
+ if (!ov) return;
+ if (ov.closing) return;
+
+ if (ov.onKey) window.removeEventListener("keydown", ov.onKey);
+
+ // animate out
+ try {
+ const mw = ov.modalWrapRef;
+ if (mw) {
+ mw.style.opacity = "0";
+ mw.style.transform = "scale(0.96)";
+ }
+ if (ov.backdrop) ov.backdrop.style.opacity = "0";
+ } catch {}
+ try { if (ov.unmountFull) ov.unmountFull(); } catch {}
+ try { if (ov.unmountMini) ov.unmountMini(); } catch {}
+ try {
+ const b = ov.backdrop;
+ if (b) setTimeout(() => { try { b.remove(); } catch {} }, 300);
+ } 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.classList.add("sw-centered-backdrop");
+ backdrop.className = "sw-centered-backdrop";
+ 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.classList.add("sw-centered-modal");
+ modalWrap.className = "post-pin--expanded sw-centered-modal";
+ modalWrap.style.pointerEvents = "auto";
+
+ // Keep size behavior from your CSS (max-width/min(92vw,...), max-height etc.)
+ modalWrap.innerHTML = `
+
+
+
+
${escapeHtml(cat)}
+
+ ${metaLeft ? `${escapeHtml(metaLeft)}` : ""}
+ ${metaRight ? `• ${escapeHtml(metaRight)}` : ""}
+ • ${escapeHtml(relTime)}
+
+
+
+
+
+
+
+ ${
+ img
+ ? `
})
`
+ : `
`
+ }
+
+
+
${escapeHtml(data?.headline || post?.title || "Untitled")}
+
+
+ ${escapeHtml(data?.summary || post?.snippet || "")}
+
+
+
+
👁 12.4k
+
❤️ 1.3k
+
💬 248
+
📍 ${escapeHtml(post?.city || post?.location_name || "Nearby")}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ backdrop.appendChild(modalWrap);
+ document.body.appendChild(backdrop);
+
+ // animate in
+ backdrop.style.opacity = "0";
+ modalWrap.style.opacity = "0";
+ modalWrap.style.transform = "scale(0.96)";
+ requestAnimationFrame(() => {
+ backdrop.style.transition = "opacity 260ms ease";
+ modalWrap.style.transition = "opacity 260ms ease, transform 320ms cubic-bezier(.2,.9,.2,1)";
+ backdrop.style.opacity = "1";
+ modalWrap.style.opacity = "1";
+ modalWrap.style.transform = "scale(1)";
+ });
+
+
+ // ✅ animate in (fade + scale)
+ try {
+ requestAnimationFrame(() => {
+ backdrop.classList.add("sw-open");
+ modalWrap.classList.add("sw-open");
+ });
+ } catch {}
+
+ // 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);
+
+ // 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 {}
+ });
+ }
+
+ // 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);
+ }
+
+ map.__swCenteredOverlay = {
+ postId: post?.id,
+ modalWrapRef: modalWrap,
+ backdrop,
+ onKey,
+ unmountFull,
+ unmountMini: null,
+ };
+}
+
+export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, theme = "blue") {
+ 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) return;
+
+ const lngLat = [lon, lat];
+
+ const root = document.createElement("div");
+ root.classList.add("sw-appear");
+ requestAnimationFrame(() => root.classList.add("sw-appear-in"));
+ root.className = "post-pin post-pin--compact";
+ root.style.zIndex = "1";
+
+ function unmountIfAny() {
+ if (root.__swUnmount) {
+ try { root.__swUnmount(); } catch {}
+ root.__swUnmount = null;
+ }
+ }
+
+ function renderCompact() {
+ unmountIfAny();
+ root.className = "post-pin post-pin--compact sw-appear sw-appear-in";
+ root.style.zIndex = "1";
+
+ root.innerHTML = `
+
+
+ `;
+
+ const wrap = root.querySelector(".sw-template-mini-wrap");
+ if (wrap) {
+ const spec = getTemplateSpecForPost(post, "mini");
+ const data = adaptPostToTemplateData(post);
+ root.__swUnmount = mountCard(wrap, spec, data, theme);
+ }
+
+ clearOcclusion(markersRef);
+ }
+
+ renderCompact();
+ root.__renderCompact = renderCompact;
+
+ const marker = new maplibregl.Marker({ element: root, anchor: "bottom" })
+ .setLngLat(lngLat)
+ .addTo(map);
+
+ markersRef.current.push({ id: post.id, marker, el: root, post });
+
+ root.addEventListener("click", (ev) => {
+ ev.stopPropagation();
+
+ const existing = map.__swCenteredOverlay;
+ if (existing && existing.postId === post?.id) {
+ closeCenteredOverlay(map);
+ return;
+ }
+
+ openCenteredOverlay({ map, post, theme });
+ if (expandedElRef) expandedElRef.current = null;
+ });
+}
diff --git a/src/components/Map/templateSpecs.js b/src/components/Map/templateSpecs.js
index efd562a..48d4908 100644
--- a/src/components/Map/templateSpecs.js
+++ b/src/components/Map/templateSpecs.js
@@ -1,8 +1,12 @@
/**
* Frontend-only template specs.
- * Pointers stay as-is:
- * - compact triangle: .post-pin-pointer-small
- * - expanded triangle: .post-card-pointer
+ * Variants:
+ * - news
+ * - breaking (news urgent)
+ * - finance (news finance)
+ * - market
+ * - friends
+ * - events
*/
export const TEMPLATE_SPECS = {
news: {
@@ -13,11 +17,9 @@ export const TEMPLATE_SPECS = {
layers: [
{ id: "badge", type: "chip", x: 12, y: 10, text: "NEWS", style: "chip.news" },
{ id: "title", type: "text", x: 12, y: 34, w: 216, h: 42, bind: "data.headline", style: "text.title", maxLines: 2 },
- { id: "meta", type: "text", x: 12, y: 76, w: 216, h: 16, bind: "data.source", style: "text.meta", maxLines: 1 }
+ { 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: 260 },
background: { type: "solid", value: "surface" },
@@ -25,18 +27,161 @@ export const TEMPLATE_SPECS = {
layers: [
{ 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 }
+ { id: "summary", type: "text", x: 16, y: 132, w: 328, h: 78, bind: "data.summary", style: "text.body", maxLines: 4 },
+ ],
+ },
+ },
+
+ breaking: {
+ mini_spec: {
+ size: { w: 240, h: 96 },
+ background: { type: "gradient", value: "breakingRed" },
+ radius: 18,
+ layers: [
+ { id: "badge", type: "chip", x: 12, y: 10, text: "BREAKING", style: "chip.danger" },
+ { id: "title", type: "text", x: 12, y: 34, w: 216, h: 42, bind: "data.headline", style: "text.title", maxLines: 2 },
+ { id: "meta", type: "text", x: 12, y: 76, w: 216, h: 16, bind: "data.source", style: "text.meta", maxLines: 1 },
+ ],
+ },
+ full_spec: {
+ size: { w: 360, h: 260 },
+ background: { type: "gradient", value: "breakingRed" },
+ radius: 22,
+ layers: [
+ { id: "badge", type: "chip", x: 16, y: 12, text: "BREAKING", style: "chip.danger" },
+ { 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 },
+ ],
+ },
+ },
+
+ finance: {
+ mini_spec: {
+ size: { w: 240, h: 96 },
+ background: { type: "gradient", value: "financeTeal" },
+ radius: 18,
+ layers: [
+ { id: "badge", type: "chip", x: 12, y: 10, text: "FINANCE", style: "chip.news" },
+ { id: "title", type: "text", x: 12, y: 34, w: 216, h: 42, bind: "data.headline", style: "text.title", maxLines: 2 },
+ { id: "meta", type: "text", x: 12, y: 76, w: 216, h: 16, bind: "data.source", style: "text.meta", maxLines: 1 },
+ ],
+ },
+ full_spec: {
+ size: { w: 360, h: 260 },
+ background: { type: "gradient", value: "financeTeal" },
+ radius: 22,
+ layers: [
+ { id: "badge", type: "chip", x: 16, y: 12, text: "FINANCE", 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 },
+ ],
+ },
+ },
+
+ market: {
+ mini_spec: {
+ size: { w: 240, h: 96 },
+ background: { type: "gradient", value: "marketGold" },
+ radius: 18,
+ layers: [
+ { id: "badge", type: "chip", x: 12, y: 10, text: "MARKET", style: "chip.news" },
+ { id: "title", type: "text", x: 12, y: 34, w: 216, h: 42, bind: "data.headline", style: "text.title", maxLines: 2 },
+ { id: "meta", type: "text", x: 12, y: 76, w: 216, h: 16, bind: "data.source", style: "text.meta", maxLines: 1 },
+ ],
+ },
+ full_spec: {
+ size: { w: 360, h: 260 },
+ background: { type: "gradient", value: "marketGold" },
+ radius: 22,
+ layers: [
+ { id: "badge", type: "chip", x: 16, y: 12, text: "MARKET", 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 },
+ ],
+ },
+ },
+
+ friends: {
+ mini_spec: {
+ size: { w: 240, h: 96 },
+ background: { type: "gradient", value: "friendsGreen" },
+ radius: 18,
+ layers: [
+ { id: "badge", type: "chip", x: 12, y: 10, text: "FRIENDS", style: "chip.news" },
+ { id: "title", type: "text", x: 12, y: 34, w: 216, h: 42, bind: "data.headline", style: "text.title", maxLines: 2 },
+ { id: "meta", type: "text", x: 12, y: 76, w: 216, h: 16, bind: "data.source", style: "text.meta", maxLines: 1 },
+ ],
+ },
+ full_spec: {
+ size: { w: 360, h: 260 },
+ background: { type: "gradient", value: "friendsGreen" },
+ radius: 22,
+ layers: [
+ { id: "badge", type: "chip", x: 16, y: 12, text: "FRIENDS", 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 },
+ ],
+ },
+ },
+
+ events: {
+ mini_spec: {
+ size: { w: 240, h: 96 },
+ background: { type: "gradient", value: "eventsPurple" },
+ radius: 18,
+ layers: [
+ { id: "badge", type: "chip", x: 12, y: 10, text: "EVENTS", style: "chip.news" },
+ { id: "title", type: "text", x: 12, y: 34, w: 216, h: 42, bind: "data.headline", style: "text.title", maxLines: 2 },
+ { id: "meta", type: "text", x: 12, y: 76, w: 216, h: 16, bind: "data.source", style: "text.meta", maxLines: 1 },
+ ],
+ },
+ full_spec: {
+ size: { w: 360, h: 260 },
+ background: { type: "gradient", value: "eventsPurple" },
+ radius: 22,
+ layers: [
+ { id: "badge", type: "chip", x: 16, y: 12, text: "EVENTS", 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 },
],
},
},
};
+function parseDateAny(raw) {
+ if (!raw) return null;
+ if (raw instanceof Date) return raw;
+ const s = String(raw).trim();
+ if (!s) return null;
+ // handle "YYYY-MM-DD HH:mm:ss"
+ const fixed = s.includes(" ") && !s.includes("T") ? s.replace(" ", "T") : s;
+ const d = new Date(fixed);
+ return Number.isNaN(d.getTime()) ? null : d;
+}
+
+function isBreaking(post) {
+ if (post?.breaking === true || post?.is_breaking === true) return true;
+
+ const t = (post?.title || "").toString().toLowerCase();
+ if (t.includes("breaking") || t.includes("urgent") || t.includes("alerte")) return true;
+
+ const d = parseDateAny(post?.created_at || post?.CreatedAt || post?.createdAt);
+ if (!d) return false;
+ const mins = (Date.now() - d.getTime()) / 60000;
+ return mins >= 0 && mins <= 45;
+}
+
function normCat(post) {
- const c = (post?.category || "").toString().toUpperCase();
- if (c === "NEWS") return "news";
- if (c === "EVENT") return "news";
- if (c === "FRIENDS") return "news";
- if (c === "MARKET") return "news";
+ const c = (post?.category || post?.Category || "").toString().toUpperCase();
+ const sub = (post?.sub_category || post?.subCategory || "").toString().toLowerCase();
+
+ if (c === "MARKET") return "market";
+ if (c === "FRIENDS") return "friends";
+ if (c === "EVENT" || c === "EVENTS") return "events";
+
+ if (c === "NEWS" && (sub.includes("finan") || sub.includes("finance"))) return "finance";
+ if (c === "NEWS" && isBreaking(post)) return "breaking";
+
return "news";
}
diff --git a/src/components/Map/useMapCore.js b/src/components/Map/useMapCore.js
index 7bfe4e6..ba2ebad 100644
--- a/src/components/Map/useMapCore.js
+++ b/src/components/Map/useMapCore.js
@@ -97,6 +97,9 @@ export function useMapCore(theme) {
map.addControl(new maplibregl.NavigationControl(), "top-right");
mapRef.current = map;
+ // ✅ Ensure viewParams is set immediately so posts load without moving the map
+ try { setViewParams(getViewFromMap(map)); } catch {}
+
const reOcclude = () => {
const el = expandedElRef.current;
if (el) applyOcclusionForExpanded(markersRef, el);
diff --git a/src/components/Map/usePostsEngine.js b/src/components/Map/usePostsEngine.js
index 963944e..a8c3923 100644
--- a/src/components/Map/usePostsEngine.js
+++ b/src/components/Map/usePostsEngine.js
@@ -4,6 +4,21 @@ import { categoryCode, matchesSubFilter, matchesTimeFilter } from "./mapFilter";
import { haversineKm } from "./mapGeo";
import { createMarkerForPost, clearAllMarkers } from "./markerManager";
+
+function getId(p) {
+ return p?.id ?? p?._id ?? null;
+}
+
+function sameIdList(a, b) {
+ if (a === b) return true;
+ if (!Array.isArray(a) || !Array.isArray(b)) return false;
+ if (a.length !== b.length) return false;
+ for (let i = 0; i < a.length; i++) {
+ if (getId(a[i]) !== getId(b[i])) return false;
+ }
+ return true;
+}
+
export function usePostsEngine({
mapRef,
viewParams,
@@ -12,51 +27,109 @@ export function usePostsEngine({
timeFilter,
markersRef,
expandedElRef,
+ theme = "blue",
}) {
const allPostsRef = useRef([]);
const lastFetchRef = useRef({ center: null, radiusKm: null, filterKey: "" });
const delayedFetchRef = useRef(null);
- const [status, setStatus] = useState("Loading posts...");
+ const [status, setStatus] = useState("");
const [visiblePosts, setVisiblePosts] = useState([]);
const [loadingPosts, setLoadingPosts] = useState(false);
const [loadError, setLoadError] = useState("");
- const rebuildMarkersForFilters = useCallback(
- (tf) => {
+ const syncMarkers = useCallback(
+ (visible) => {
const map = mapRef.current;
if (!map) return;
- // ✅ If expanded popup is open, NEVER rebuild markers (rebuild would close it)
+ // If user is reading an expanded overlay, don't touch markers
if (expandedElRef?.current) return;
- clearAllMarkers(markersRef, expandedElRef);
+ const wanted = new Map();
+ for (const post of visible) {
+ const id = getId(post);
+ if (id != null) wanted.set(id, post);
+ }
+ const cur = markersRef.current || [];
+ const next = [];
+
+ // remove markers not wanted (smooth fade-out)
+ for (const item of cur) {
+ const id = item?.id;
+ if (id == null || !wanted.has(id)) {
+ try {
+ const el = item?.el;
+ if (el) {
+ el.classList.add("sw-disappear");
+ el.style.pointerEvents = "none";
+ }
+ } catch {}
+
+ // let CSS transition play, then remove from map
+ setTimeout(() => {
+ try { if (item?.el && item.el.__swUnmount) item.el.__swUnmount(); } catch {}
+ try { item?.marker?.remove?.(); } catch {}
+ }, 840);
+ } else {
+ next.push(item);
+ }
+ }
+
+ markersRef.current = next;
+
+ const have = new Set(next.map((x) => x.id));
+
+ // add missing markers
+ for (const post of visible) {
+ const id = getId(post);
+ if (id == null || have.has(id)) continue;
+ createMarkerForPost(post, mapRef, markersRef, expandedElRef, theme);
+ }
+ },
+ [mapRef, markersRef, expandedElRef, theme]
+ );
+
+ const computeVisible = useCallback(
+ (tf) => {
const posts = allPostsRef.current || [];
const catCode = categoryCode(mainFilter);
- const visible = posts.filter((p) => {
+ return posts.filter((p) => {
if (catCode) {
const pc = (p.category || p.Category || "").toString().toUpperCase();
if (pc !== catCode) return false;
}
if (!matchesSubFilter(p, subFilter)) return false;
- if (!matchesTimeFilter(p.created_at || p.CreatedAt, tf)) return false;
+ if (!matchesTimeFilter(p.created_at || p.CreatedAt || p.createdAt, tf)) return false;
return true;
});
-
- visible.forEach((post) => createMarkerForPost(post, mapRef, markersRef, expandedElRef));
-
- setVisiblePosts(visible);
- setStatus(visible.length ? "" : "No posts found.");
},
- [mapRef, markersRef, expandedElRef, mainFilter, subFilter]
+ [mainFilter, subFilter]
);
- useEffect(() => {
- rebuildMarkersForFilters(timeFilter);
- }, [timeFilter, mainFilter, subFilter, rebuildMarkersForFilters]);
+ const refreshVisibleAndMarkers = useCallback(
+ (tf) => {
+ const v = computeVisible(tf);
+ // reduce wall blink: don't update if same ids
+ setVisiblePosts((prev) => (sameIdList(prev, v) ? prev : v));
+
+ // no annoying "No posts found" bubble
+ setStatus("");
+
+ syncMarkers(v);
+ },
+ [computeVisible, syncMarkers]
+ );
+
+ // On filter changes: recompute + sync (NO clear-all rebuild)
+ useEffect(() => {
+ refreshVisibleAndMarkers(timeFilter);
+ }, [timeFilter, mainFilter, subFilter, refreshVisibleAndMarkers]);
+
+ // Fetch on view change (moveend)
useEffect(() => {
const map = mapRef.current;
if (!map) return;
@@ -67,18 +140,18 @@ export function usePostsEngine({
async function load() {
const mapObj = mapRef.current;
- // ✅ If expanded popup is open: delay fetch (do NOT clear/rebuild while user reads)
+ // If expanded overlay is open: delay fetch (do NOT disturb UI)
if (expandedElRef?.current) {
if (delayedFetchRef.current) clearTimeout(delayedFetchRef.current);
- delayedFetchRef.current = setTimeout(load, 2500);
+ delayedFetchRef.current = setTimeout(load, 1200);
return;
}
- // Also respect ignore window (during auto-pan)
+ // Respect ignore window if any
try {
const until = mapObj?.__swIgnoreFetchUntil || 0;
if (until && Date.now() < until) {
- const wait = Math.min(4000, until - Date.now());
+ const wait = Math.min(2500, until - Date.now());
if (delayedFetchRef.current) clearTimeout(delayedFetchRef.current);
delayedFetchRef.current = setTimeout(load, wait + 25);
return;
@@ -93,18 +166,17 @@ export function usePostsEngine({
if (last.center && last.filterKey === filterKey) {
const [lastLng, lastLat] = last.center;
- const distKm = haversineKm(lastLat, lastLng, viewParams.center[1], lng);
+ const distKm = haversineKm(lastLat, lastLng, lat, lng);
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);
+ const minMoveKm = Math.max(40, radiusKm * 0.22);
if (!radiusChanged && distKm < minMoveKm) return;
}
try {
- setStatus("Loading posts...");
setLoadingPosts(true);
setLoadError("");
@@ -126,47 +198,27 @@ export function usePostsEngine({
const existing = allPostsRef.current || [];
const byId = new Map();
for (const p of existing) {
- if (p && typeof p.id !== "undefined") byId.set(p.id, p);
+ const id = getId(p);
+ if (id != null) byId.set(id, p);
}
if (Array.isArray(newPosts)) {
for (const p of newPosts) {
- if (p && typeof p.id !== "undefined" && !byId.has(p.id)) byId.set(p.id, p);
+ const id = getId(p);
+ if (id != null && !byId.has(id)) byId.set(id, p);
}
}
allPostsRef.current = Array.from(byId.values());
lastFetchRef.current = { center: [...viewParams.center], radiusKm, filterKey };
- // If expanded popup is open, don't rebuild markers (keeps it open)
- try {
- if (expandedElRef?.current) {
- const posts = allPostsRef.current || [];
- const catCode2 = categoryCode(mainFilter);
+ // Smooth update: sync markers + wall without clearing everything
+ refreshVisibleAndMarkers(timeFilter);
- 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);
if (!cancelled) {
- setStatus("No posts found.");
- setLoadError("");
+ setLoadError("Error loading posts.");
setLoadingPosts(false);
}
}
@@ -180,7 +232,7 @@ export function usePostsEngine({
delayedFetchRef.current = null;
}
};
- }, [viewParams, mainFilter, subFilter, mapRef, rebuildMarkersForFilters, timeFilter, expandedElRef]);
+ }, [viewParams, mainFilter, subFilter, mapRef, timeFilter, expandedElRef, refreshVisibleAndMarkers]);
const handleIncomingPost = useCallback(
(p) => {
@@ -192,12 +244,21 @@ export function usePostsEngine({
if (pc !== catCode) return;
}
if (!matchesSubFilter(p, subFilter)) return;
- if (!matchesTimeFilter(p.created_at || p.CreatedAt, timeFilter)) return;
+ if (!matchesTimeFilter(p.created_at || p.CreatedAt || p.createdAt, timeFilter)) return;
- createMarkerForPost(p, mapRef, markersRef, expandedElRef);
- setVisiblePosts((current) => [...current, p]);
+ // add marker if missing
+ const id = getId(p);
+ const have = new Set((markersRef.current || []).map((x) => x.id));
+ if (id != null && !have.has(id)) {
+ createMarkerForPost(p, mapRef, markersRef, expandedElRef, theme);
+ }
+
+ setVisiblePosts((current) => {
+ const next = [...current, p];
+ return next;
+ });
},
- [mainFilter, subFilter, timeFilter, mapRef, markersRef, expandedElRef]
+ [mainFilter, subFilter, timeFilter, mapRef, markersRef, expandedElRef, theme]
);
return { status, visiblePosts, loadingPosts, loadError, handleIncomingPost };
diff --git a/src/components/Map/usePostsEngine.js.bak b/src/components/Map/usePostsEngine.js.bak
new file mode 100644
index 0000000..963944e
--- /dev/null
+++ b/src/components/Map/usePostsEngine.js.bak
@@ -0,0 +1,204 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import { fetchPosts } from "../../api/client";
+import { categoryCode, matchesSubFilter, matchesTimeFilter } from "./mapFilter";
+import { haversineKm } from "./mapGeo";
+import { createMarkerForPost, clearAllMarkers } from "./markerManager";
+
+export function usePostsEngine({
+ mapRef,
+ viewParams,
+ mainFilter,
+ subFilter,
+ timeFilter,
+ markersRef,
+ expandedElRef,
+}) {
+ const allPostsRef = useRef([]);
+ const lastFetchRef = useRef({ center: null, radiusKm: null, filterKey: "" });
+ const delayedFetchRef = useRef(null);
+
+ const [status, setStatus] = useState("Loading posts...");
+ const [visiblePosts, setVisiblePosts] = useState([]);
+ const [loadingPosts, setLoadingPosts] = useState(false);
+ const [loadError, setLoadError] = useState("");
+
+ const rebuildMarkersForFilters = useCallback(
+ (tf) => {
+ const map = mapRef.current;
+ if (!map) return;
+
+ // ✅ If expanded popup is open, NEVER rebuild markers (rebuild would close it)
+ if (expandedElRef?.current) return;
+
+ clearAllMarkers(markersRef, expandedElRef);
+
+ const posts = allPostsRef.current || [];
+ const catCode = categoryCode(mainFilter);
+
+ const visible = posts.filter((p) => {
+ if (catCode) {
+ const pc = (p.category || p.Category || "").toString().toUpperCase();
+ if (pc !== catCode) return false;
+ }
+ if (!matchesSubFilter(p, subFilter)) return false;
+ if (!matchesTimeFilter(p.created_at || p.CreatedAt, tf)) return false;
+ return true;
+ });
+
+ visible.forEach((post) => createMarkerForPost(post, mapRef, markersRef, expandedElRef));
+
+ setVisiblePosts(visible);
+ setStatus(visible.length ? "" : "No posts found.");
+ },
+ [mapRef, markersRef, expandedElRef, mainFilter, subFilter]
+ );
+
+ useEffect(() => {
+ rebuildMarkersForFilters(timeFilter);
+ }, [timeFilter, mainFilter, subFilter, rebuildMarkersForFilters]);
+
+ useEffect(() => {
+ const map = mapRef.current;
+ if (!map) return;
+ if (!viewParams.center) return;
+
+ let cancelled = false;
+
+ async function load() {
+ 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, 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}`;
+
+ const last = lastFetchRef.current;
+
+ if (last.center && last.filterKey === filterKey) {
+ const [lastLng, lastLat] = last.center;
+ const distKm = haversineKm(lastLat, lastLng, viewParams.center[1], lng);
+
+ 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;
+ }
+
+ try {
+ setStatus("Loading posts...");
+ setLoadingPosts(true);
+ setLoadError("");
+
+ const catCode = categoryCode(mainFilter);
+ const subCatParam =
+ subFilter && subFilter.toUpperCase() !== "ALL" && subFilter !== "All" ? subFilter : "";
+
+ const newPosts = await fetchPosts({
+ category: catCode,
+ subCategory: subCatParam,
+ time: "",
+ lat,
+ lon: lng,
+ radiusKm,
+ });
+
+ if (cancelled) return;
+
+ const existing = allPostsRef.current || [];
+ const byId = new Map();
+ for (const p of existing) {
+ if (p && typeof p.id !== "undefined") byId.set(p.id, p);
+ }
+ if (Array.isArray(newPosts)) {
+ for (const p of newPosts) {
+ if (p && typeof p.id !== "undefined" && !byId.has(p.id)) byId.set(p.id, p);
+ }
+ }
+
+ allPostsRef.current = Array.from(byId.values());
+ lastFetchRef.current = { center: [...viewParams.center], radiusKm, filterKey };
+
+ // 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);
+ if (!cancelled) {
+ setStatus("No posts found.");
+ setLoadError("");
+ setLoadingPosts(false);
+ }
+ }
+ }
+
+ load();
+ return () => {
+ cancelled = true;
+ if (delayedFetchRef.current) {
+ try { clearTimeout(delayedFetchRef.current); } catch {}
+ delayedFetchRef.current = null;
+ }
+ };
+ }, [viewParams, mainFilter, subFilter, mapRef, rebuildMarkersForFilters, timeFilter, expandedElRef]);
+
+ const handleIncomingPost = useCallback(
+ (p) => {
+ allPostsRef.current = [...allPostsRef.current, p];
+
+ const catCode = categoryCode(mainFilter);
+ if (catCode) {
+ const pc = (p.category || p.Category || "").toString().toUpperCase();
+ if (pc !== catCode) return;
+ }
+ if (!matchesSubFilter(p, subFilter)) return;
+ if (!matchesTimeFilter(p.created_at || p.CreatedAt, timeFilter)) return;
+
+ createMarkerForPost(p, mapRef, markersRef, expandedElRef);
+ setVisiblePosts((current) => [...current, p]);
+ },
+ [mainFilter, subFilter, timeFilter, mapRef, markersRef, expandedElRef]
+ );
+
+ return { status, visiblePosts, loadingPosts, loadError, handleIncomingPost };
+}
diff --git a/src/components/Map/usePostsEngine.js.bak2 b/src/components/Map/usePostsEngine.js.bak2
new file mode 100644
index 0000000..85040f4
--- /dev/null
+++ b/src/components/Map/usePostsEngine.js.bak2
@@ -0,0 +1,253 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import { fetchPosts } from "../../api/client";
+import { categoryCode, matchesSubFilter, matchesTimeFilter } from "./mapFilter";
+import { haversineKm } from "./mapGeo";
+import { createMarkerForPost, clearAllMarkers } from "./markerManager";
+
+function getId(p) {
+ return p?.id ?? p?._id ?? null;
+}
+
+function sameIdList(a, b) {
+ if (a === b) return true;
+ if (!Array.isArray(a) || !Array.isArray(b)) return false;
+ if (a.length !== b.length) return false;
+ for (let i = 0; i < a.length; i++) {
+ if (getId(a[i]) !== getId(b[i])) return false;
+ }
+ return true;
+}
+
+export function usePostsEngine({
+ mapRef,
+ viewParams,
+ mainFilter,
+ subFilter,
+ timeFilter,
+ markersRef,
+ expandedElRef,
+ theme = "blue",
+}) {
+ const allPostsRef = useRef([]);
+ const lastFetchRef = useRef({ center: null, radiusKm: null, filterKey: "" });
+ const delayedFetchRef = useRef(null);
+
+ const [status, setStatus] = useState("");
+ const [visiblePosts, setVisiblePosts] = useState([]);
+ const [loadingPosts, setLoadingPosts] = useState(false);
+ const [loadError, setLoadError] = useState("");
+
+ const syncMarkers = useCallback(
+ (visible) => {
+ const map = mapRef.current;
+ if (!map) return;
+
+ // If user is reading an expanded overlay, don't touch markers
+ if (expandedElRef?.current) return;
+
+ const wanted = new Map();
+ for (const post of visible) {
+ const id = getId(post);
+ if (id != null) wanted.set(id, post);
+ }
+
+ const cur = markersRef.current || [];
+ const next = [];
+
+ // remove markers not wanted
+ for (const item of cur) {
+ const id = item?.id;
+ if (id == null || !wanted.has(id)) {
+ try { if (item?.el && item.el.__swUnmount) item.el.__swUnmount(); } catch {}
+ try { item?.marker?.remove?.(); } catch {}
+ } else {
+ next.push(item);
+ }
+ }
+
+ markersRef.current = next;
+
+ const have = new Set(next.map((x) => x.id));
+
+ // add missing markers
+ for (const post of visible) {
+ const id = getId(post);
+ if (id == null || have.has(id)) continue;
+ createMarkerForPost(post, mapRef, markersRef, expandedElRef, theme);
+ }
+ },
+ [mapRef, markersRef, expandedElRef, theme]
+ );
+
+ const computeVisible = useCallback(
+ (tf) => {
+ const posts = allPostsRef.current || [];
+ const catCode = categoryCode(mainFilter);
+
+ return posts.filter((p) => {
+ if (catCode) {
+ const pc = (p.category || p.Category || "").toString().toUpperCase();
+ if (pc !== catCode) return false;
+ }
+ if (!matchesSubFilter(p, subFilter)) return false;
+ if (!matchesTimeFilter(p.created_at || p.CreatedAt || p.createdAt, tf)) return false;
+ return true;
+ });
+ },
+ [mainFilter, subFilter]
+ );
+
+ const refreshVisibleAndMarkers = useCallback(
+ (tf) => {
+ const v = computeVisible(tf);
+
+ // reduce wall blink: don't update if same ids
+ setVisiblePosts((prev) => (sameIdList(prev, v) ? prev : v));
+
+ // no annoying "No posts found" bubble
+ setStatus("");
+
+ syncMarkers(v);
+ },
+ [computeVisible, syncMarkers]
+ );
+
+ // On filter changes: recompute + sync (NO clear-all rebuild)
+ useEffect(() => {
+ refreshVisibleAndMarkers(timeFilter);
+ }, [timeFilter, mainFilter, subFilter, refreshVisibleAndMarkers]);
+
+ // Fetch on view change (moveend)
+ useEffect(() => {
+ const map = mapRef.current;
+ if (!map) return;
+ if (!viewParams.center) return;
+
+ let cancelled = false;
+
+ async function load() {
+ const mapObj = mapRef.current;
+
+ // If expanded overlay is open: delay fetch (do NOT disturb UI)
+ if (expandedElRef?.current) {
+ if (delayedFetchRef.current) clearTimeout(delayedFetchRef.current);
+ delayedFetchRef.current = setTimeout(load, 1200);
+ return;
+ }
+
+ // Respect ignore window if any
+ try {
+ const until = mapObj?.__swIgnoreFetchUntil || 0;
+ if (until && Date.now() < until) {
+ const wait = Math.min(2500, 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}`;
+
+ const last = lastFetchRef.current;
+
+ if (last.center && last.filterKey === filterKey) {
+ const [lastLng, lastLat] = last.center;
+ const distKm = haversineKm(lastLat, lastLng, lat, lng);
+
+ const lastR = last.radiusKm || 0;
+ const radiusChanged = !lastR || Math.abs(radiusKm - lastR) / Math.max(lastR, 1) >= 0.25;
+
+ const minMoveKm = Math.max(40, radiusKm * 0.22);
+
+ if (!radiusChanged && distKm < minMoveKm) return;
+ }
+
+ try {
+ setLoadingPosts(true);
+ setLoadError("");
+
+ const catCode = categoryCode(mainFilter);
+ const subCatParam =
+ subFilter && subFilter.toUpperCase() !== "ALL" && subFilter !== "All" ? subFilter : "";
+
+ const newPosts = await fetchPosts({
+ category: catCode,
+ subCategory: subCatParam,
+ time: "",
+ lat,
+ lon: lng,
+ radiusKm,
+ });
+
+ if (cancelled) return;
+
+ const existing = allPostsRef.current || [];
+ const byId = new Map();
+ for (const p of existing) {
+ const id = getId(p);
+ if (id != null) byId.set(id, p);
+ }
+ if (Array.isArray(newPosts)) {
+ for (const p of newPosts) {
+ const id = getId(p);
+ if (id != null && !byId.has(id)) byId.set(id, p);
+ }
+ }
+
+ allPostsRef.current = Array.from(byId.values());
+ lastFetchRef.current = { center: [...viewParams.center], radiusKm, filterKey };
+
+ // Smooth update: sync markers + wall without clearing everything
+ refreshVisibleAndMarkers(timeFilter);
+
+ setLoadingPosts(false);
+ } catch (err) {
+ console.error("Erreur chargement posts:", err);
+ if (!cancelled) {
+ setLoadError("Error loading posts.");
+ setLoadingPosts(false);
+ }
+ }
+ }
+
+ load();
+ return () => {
+ cancelled = true;
+ if (delayedFetchRef.current) {
+ try { clearTimeout(delayedFetchRef.current); } catch {}
+ delayedFetchRef.current = null;
+ }
+ };
+ }, [viewParams, mainFilter, subFilter, mapRef, timeFilter, expandedElRef, refreshVisibleAndMarkers]);
+
+ const handleIncomingPost = useCallback(
+ (p) => {
+ allPostsRef.current = [...allPostsRef.current, p];
+
+ const catCode = categoryCode(mainFilter);
+ if (catCode) {
+ const pc = (p.category || p.Category || "").toString().toUpperCase();
+ if (pc !== catCode) return;
+ }
+ if (!matchesSubFilter(p, subFilter)) return;
+ if (!matchesTimeFilter(p.created_at || p.CreatedAt || p.createdAt, timeFilter)) return;
+
+ // add marker if missing
+ const id = getId(p);
+ const have = new Set((markersRef.current || []).map((x) => x.id));
+ if (id != null && !have.has(id)) {
+ createMarkerForPost(p, mapRef, markersRef, expandedElRef, theme);
+ }
+
+ setVisiblePosts((current) => {
+ const next = [...current, p];
+ return next;
+ });
+ },
+ [mainFilter, subFilter, timeFilter, mapRef, markersRef, expandedElRef, theme]
+ );
+
+ return { status, visiblePosts, loadingPosts, loadError, handleIncomingPost };
+}
diff --git a/src/components/Map/usePostsEngine.js.bak3 b/src/components/Map/usePostsEngine.js.bak3
new file mode 100644
index 0000000..11e4c44
--- /dev/null
+++ b/src/components/Map/usePostsEngine.js.bak3
@@ -0,0 +1,264 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import { fetchPosts } from "../../api/client";
+import { categoryCode, matchesSubFilter, matchesTimeFilter } from "./mapFilter";
+import { haversineKm } from "./mapGeo";
+import { createMarkerForPost, clearAllMarkers } from "./markerManager";
+
+function getId(p) {
+ return p?.id ?? p?._id ?? null;
+}
+
+function sameIdList(a, b) {
+ if (a === b) return true;
+ if (!Array.isArray(a) || !Array.isArray(b)) return false;
+ if (a.length !== b.length) return false;
+ for (let i = 0; i < a.length; i++) {
+ if (getId(a[i]) !== getId(b[i])) return false;
+ }
+ return true;
+}
+
+export function usePostsEngine({
+ mapRef,
+ viewParams,
+ mainFilter,
+ subFilter,
+ timeFilter,
+ markersRef,
+ expandedElRef,
+ theme = "blue",
+}) {
+ const allPostsRef = useRef([]);
+ const lastFetchRef = useRef({ center: null, radiusKm: null, filterKey: "" });
+ const delayedFetchRef = useRef(null);
+
+ const [status, setStatus] = useState("");
+ const [visiblePosts, setVisiblePosts] = useState([]);
+ const [loadingPosts, setLoadingPosts] = useState(false);
+ const [loadError, setLoadError] = useState("");
+
+ const syncMarkers = useCallback(
+ (visible) => {
+ const map = mapRef.current;
+ if (!map) return;
+
+ // If user is reading an expanded overlay, don't touch markers
+ if (expandedElRef?.current) return;
+
+ const wanted = new Map();
+ for (const post of visible) {
+ const id = getId(post);
+ if (id != null) wanted.set(id, post);
+ }
+
+ const cur = markersRef.current || [];
+ const next = [];
+
+ // remove markers not wanted (smooth fade-out)
+ for (const item of cur) {
+ const id = item?.id;
+ if (id == null || !wanted.has(id)) {
+ try {
+ const el = item?.el;
+ if (el) {
+ el.classList.add("sw-disappear");
+ el.style.pointerEvents = "none";
+ }
+ } catch {}
+
+ // let CSS transition play, then remove from map
+ setTimeout(() => {
+ try { if (item?.el && item.el.__swUnmount) item.el.__swUnmount(); } catch {}
+ try { item?.marker?.remove?.(); } catch {}
+ }, 280);
+ } else {
+ next.push(item);
+ }
+ }
+
+ markersRef.current = next;
+
+ const have = new Set(next.map((x) => x.id));
+
+ // add missing markers
+ for (const post of visible) {
+ const id = getId(post);
+ if (id == null || have.has(id)) continue;
+ createMarkerForPost(post, mapRef, markersRef, expandedElRef, theme);
+ }
+ },
+ [mapRef, markersRef, expandedElRef, theme]
+ );
+
+ const computeVisible = useCallback(
+ (tf) => {
+ const posts = allPostsRef.current || [];
+ const catCode = categoryCode(mainFilter);
+
+ return posts.filter((p) => {
+ if (catCode) {
+ const pc = (p.category || p.Category || "").toString().toUpperCase();
+ if (pc !== catCode) return false;
+ }
+ if (!matchesSubFilter(p, subFilter)) return false;
+ if (!matchesTimeFilter(p.created_at || p.CreatedAt || p.createdAt, tf)) return false;
+ return true;
+ });
+ },
+ [mainFilter, subFilter]
+ );
+
+ const refreshVisibleAndMarkers = useCallback(
+ (tf) => {
+ const v = computeVisible(tf);
+
+ // reduce wall blink: don't update if same ids
+ setVisiblePosts((prev) => (sameIdList(prev, v) ? prev : v));
+
+ // no annoying "No posts found" bubble
+ setStatus("");
+
+ syncMarkers(v);
+ },
+ [computeVisible, syncMarkers]
+ );
+
+ // On filter changes: recompute + sync (NO clear-all rebuild)
+ useEffect(() => {
+ refreshVisibleAndMarkers(timeFilter);
+ }, [timeFilter, mainFilter, subFilter, refreshVisibleAndMarkers]);
+
+ // Fetch on view change (moveend)
+ useEffect(() => {
+ const map = mapRef.current;
+ if (!map) return;
+ if (!viewParams.center) return;
+
+ let cancelled = false;
+
+ async function load() {
+ const mapObj = mapRef.current;
+
+ // If expanded overlay is open: delay fetch (do NOT disturb UI)
+ if (expandedElRef?.current) {
+ if (delayedFetchRef.current) clearTimeout(delayedFetchRef.current);
+ delayedFetchRef.current = setTimeout(load, 1200);
+ return;
+ }
+
+ // Respect ignore window if any
+ try {
+ const until = mapObj?.__swIgnoreFetchUntil || 0;
+ if (until && Date.now() < until) {
+ const wait = Math.min(2500, 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}`;
+
+ const last = lastFetchRef.current;
+
+ if (last.center && last.filterKey === filterKey) {
+ const [lastLng, lastLat] = last.center;
+ const distKm = haversineKm(lastLat, lastLng, lat, lng);
+
+ const lastR = last.radiusKm || 0;
+ const radiusChanged = !lastR || Math.abs(radiusKm - lastR) / Math.max(lastR, 1) >= 0.25;
+
+ const minMoveKm = Math.max(40, radiusKm * 0.22);
+
+ if (!radiusChanged && distKm < minMoveKm) return;
+ }
+
+ try {
+ setLoadingPosts(true);
+ setLoadError("");
+
+ const catCode = categoryCode(mainFilter);
+ const subCatParam =
+ subFilter && subFilter.toUpperCase() !== "ALL" && subFilter !== "All" ? subFilter : "";
+
+ const newPosts = await fetchPosts({
+ category: catCode,
+ subCategory: subCatParam,
+ time: "",
+ lat,
+ lon: lng,
+ radiusKm,
+ });
+
+ if (cancelled) return;
+
+ const existing = allPostsRef.current || [];
+ const byId = new Map();
+ for (const p of existing) {
+ const id = getId(p);
+ if (id != null) byId.set(id, p);
+ }
+ if (Array.isArray(newPosts)) {
+ for (const p of newPosts) {
+ const id = getId(p);
+ if (id != null && !byId.has(id)) byId.set(id, p);
+ }
+ }
+
+ allPostsRef.current = Array.from(byId.values());
+ lastFetchRef.current = { center: [...viewParams.center], radiusKm, filterKey };
+
+ // Smooth update: sync markers + wall without clearing everything
+ refreshVisibleAndMarkers(timeFilter);
+
+ setLoadingPosts(false);
+ } catch (err) {
+ console.error("Erreur chargement posts:", err);
+ if (!cancelled) {
+ setLoadError("Error loading posts.");
+ setLoadingPosts(false);
+ }
+ }
+ }
+
+ load();
+ return () => {
+ cancelled = true;
+ if (delayedFetchRef.current) {
+ try { clearTimeout(delayedFetchRef.current); } catch {}
+ delayedFetchRef.current = null;
+ }
+ };
+ }, [viewParams, mainFilter, subFilter, mapRef, timeFilter, expandedElRef, refreshVisibleAndMarkers]);
+
+ const handleIncomingPost = useCallback(
+ (p) => {
+ allPostsRef.current = [...allPostsRef.current, p];
+
+ const catCode = categoryCode(mainFilter);
+ if (catCode) {
+ const pc = (p.category || p.Category || "").toString().toUpperCase();
+ if (pc !== catCode) return;
+ }
+ if (!matchesSubFilter(p, subFilter)) return;
+ if (!matchesTimeFilter(p.created_at || p.CreatedAt || p.createdAt, timeFilter)) return;
+
+ // add marker if missing
+ const id = getId(p);
+ const have = new Set((markersRef.current || []).map((x) => x.id));
+ if (id != null && !have.has(id)) {
+ createMarkerForPost(p, mapRef, markersRef, expandedElRef, theme);
+ }
+
+ setVisiblePosts((current) => {
+ const next = [...current, p];
+ return next;
+ });
+ },
+ [mainFilter, subFilter, timeFilter, mapRef, markersRef, expandedElRef, theme]
+ );
+
+ return { status, visiblePosts, loadingPosts, loadError, handleIncomingPost };
+}
diff --git a/src/components/Posts/PostList.jsx b/src/components/Posts/PostList.jsx
index 3094a15..ee5e569 100644
--- a/src/components/Posts/PostList.jsx
+++ b/src/components/Posts/PostList.jsx
@@ -35,8 +35,7 @@ export default function PostList({ posts, loading, error, selectedPost, onSelect
- {loading && Chargement...
}
- {error && {error}
}
+ {error && {error}
}
{filtered.map((p) => (
+ posts.filter((p) => {
+ const km = getKmFromPost(p);
+ if (km == null) return true;
+ return km <= kmFilter;
+ }),
+ [posts, kmFilter]
+ );
+
+ return (
+
+
+
+
+ setKmFilter(Number(e.target.value))}
+ />
+
+
+
+ {loading &&
Chargement...
}
+ {error &&
{error}
}
+
+ {filtered.map((p) => (
+
+ ))}
+
+ {!loading && filtered.length === 0 &&
Aucun post dans ce rayon.
}
+
+ );
+}
diff --git a/src/styles/filters.css b/src/styles/filters.css
index 158c9da..c4da7c4 100644
--- a/src/styles/filters.css
+++ b/src/styles/filters.css
@@ -6,7 +6,7 @@
border: 1.5px solid rgba(148, 163, 184, 0.7);
display:flex; align-items:center; justify-content:center;
box-shadow:0 3px 10px rgba(0,0,0,.75);
- transition: transform .12s ease, box-shadow .12s ease, border-color .12s ease, background .12s ease;
+ transition: transform 0.36s ease, box-shadow 0.36s ease, border-color 0.36s ease, background 0.36s ease;
}
.sw-icon-circle i { font-size:22px; }
.sw-icon-label { font-size:.7rem; line-height:1; text-shadow:0 1px 2px rgba(0,0,0,.9); }
diff --git a/src/styles/filters.css.bak3 b/src/styles/filters.css.bak3
new file mode 100644
index 0000000..158c9da
--- /dev/null
+++ b/src/styles/filters.css.bak3
@@ -0,0 +1,139 @@
+.sw-icon-column { display:flex; flex-direction:column; gap:.55rem; align-items:center; }
+.sw-icon-btn { background:none; border:none; padding:0; cursor:pointer; display:flex; flex-direction:column; align-items:center; gap:.15rem; color:#e5e7eb; }
+.sw-icon-circle {
+ width:54px; height:54px; border-radius:50%;
+ background: rgba(15, 23, 42, 0.86);
+ border: 1.5px solid rgba(148, 163, 184, 0.7);
+ display:flex; align-items:center; justify-content:center;
+ box-shadow:0 3px 10px rgba(0,0,0,.75);
+ transition: transform .12s ease, box-shadow .12s ease, border-color .12s ease, background .12s ease;
+}
+.sw-icon-circle i { font-size:22px; }
+.sw-icon-label { font-size:.7rem; line-height:1; text-shadow:0 1px 2px rgba(0,0,0,.9); }
+.sw-icon-btn:hover .sw-icon-circle { transform: translateY(-1px); box-shadow:0 5px 14px rgba(0,0,0,.85); }
+.sw-icon-btn:active .sw-icon-circle { transform: scale(.96); box-shadow:0 2px 8px rgba(0,0,0,.9); }
+.sw-icon-active .sw-icon-circle { background: rgba(37, 99, 235, 0.82); border-color:#60a5fa; box-shadow:0 0 14px rgba(56,189,248,.95); }
+.sw-icon-active .sw-icon-label { color:#bfdbfe; font-weight:600; }
+
+.sw-bottom-row {
+ display:flex; gap:.35rem; padding:.2rem .6rem;
+ background: rgba(15, 23, 42, 0.94);
+ border-radius: 999px;
+ box-shadow: 0 4px 16px rgba(0,0,0,.85);
+}
+.sw-bottom-item { border:none; background:none; padding:0; cursor:pointer; display:flex; flex-direction:column; align-items:center; gap:.12rem; min-width:54px; }
+.sw-bottom-circle {
+ width:40px; height:40px; border-radius:50%;
+ background: rgba(15, 23, 42, 0.86);
+ border: 1.5px solid rgba(148, 163, 184, 0.6);
+ display:flex; align-items:center; justify-content:center;
+ font-size:.78rem;
+ box-shadow:0 3px 10px rgba(0,0,0,.75);
+}
+
+/* SW_SUBCAT_ICON_CSS */
+.sw-bottom-circle i{
+ font-size: 18px;
+ line-height: 1;
+}
+/* SW_SUBCAT_ICON_CSS:END */
+.sw-bottom-label { font-size:.65rem; color:#e5e7eb; text-shadow:0 1px 2px rgba(0,0,0,.9); }
+.sw-bottom-active .sw-bottom-circle { background: rgba(37, 99, 235, 0.82); border-color:#60a5fa; }
+.sw-bottom-active .sw-bottom-label { color:#bfdbfe; font-weight:600; }
+
+/* Floating subcategory dock (always above Sociowall, not coverable) */
+.sw-subcats-float{
+ position: fixed;
+ left: 50%;
+ transform: translateX(-50%);
+ bottom: calc(14px + var(--sw-wall-overlap, 0px));
+ z-index: 300;
+ pointer-events: none;
+}
+
+.sw-subcats-float .sw-bottom-row{
+ pointer-events: auto;
+ max-width: min(980px, 96vw);
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+}
+
+.sw-subcats-float .sw-bottom-row::-webkit-scrollbar{ height: 0; }
+
+/* SW_FILTER_ICON_VISIBILITY:BEGIN */
+
+/* Make subcategory icons inherit color (so they show) */
+.sw-bottom-item{ color:#e5e7eb; }
+.sw-bottom-circle{ color:#e5e7eb; }
+.sw-bottom-circle i{
+ color: currentColor;
+ font-size: 20px; /* closer to right-side icons feel */
+ line-height: 1;
+ opacity: .96;
+ filter: drop-shadow(0 1px 2px rgba(0,0,0,.65));
+}
+
+/* Active state: brighter icon/text like category buttons */
+.sw-bottom-active .sw-bottom-circle{
+ color:#dbeafe;
+ box-shadow:0 0 14px rgba(56,189,248,.60);
+ border-color: rgba(96,165,250,.95);
+}
+.sw-bottom-active .sw-bottom-label{
+ color:#bfdbfe;
+ font-weight:700;
+}
+
+/* ---------- LIGHT THEME FIXES (readable) ---------- */
+body[data-theme="light"] .sw-bottom-row{
+ background: rgba(255,255,255,0.95);
+ border: 1px solid rgba(148,163,184,0.85);
+ box-shadow: 0 6px 18px rgba(0,0,0,.12);
+}
+
+body[data-theme="light"] .sw-bottom-item{ color:#0B1220; }
+body[data-theme="light"] .sw-bottom-circle{
+ background: rgba(255,255,255,0.92);
+ border-color: rgba(148,163,184,0.85);
+ box-shadow: 0 3px 10px rgba(0,0,0,.10);
+ color:#0B1220;
+}
+body[data-theme="light"] .sw-bottom-label{
+ color:#0B1220;
+ text-shadow:none;
+}
+
+body[data-theme="light"] .sw-bottom-active .sw-bottom-circle{
+ background: rgba(30,107,255,0.92);
+ border-color: rgba(30,107,255,0.95);
+ color:#ffffff;
+ box-shadow:0 0 14px rgba(30,107,255,.35);
+}
+body[data-theme="light"] .sw-bottom-active .sw-bottom-label{
+ color:#0B1220;
+ font-weight:800;
+}
+
+/* Also fix right-side category buttons in light theme */
+body[data-theme="light"] .sw-icon-btn{ color:#0B1220; }
+body[data-theme="light"] .sw-icon-circle{
+ background: rgba(255,255,255,0.92);
+ border-color: rgba(148,163,184,0.85);
+ box-shadow: 0 3px 10px rgba(0,0,0,.10);
+}
+body[data-theme="light"] .sw-icon-label{
+ color:#0B1220;
+ text-shadow:none;
+}
+body[data-theme="light"] .sw-icon-active .sw-icon-circle{
+ background: rgba(30,107,255,0.92);
+ border-color: rgba(30,107,255,0.95);
+ box-shadow:0 0 14px rgba(30,107,255,.35);
+}
+body[data-theme="light"] .sw-icon-active .sw-icon-label{
+ color:#0B1220;
+ font-weight:800;
+}
+
+/* SW_FILTER_ICON_VISIBILITY:END */
+
diff --git a/src/styles/mapMarkers.css b/src/styles/mapMarkers.css
index 391ea0e..0275996 100644
--- a/src/styles/mapMarkers.css
+++ b/src/styles/mapMarkers.css
@@ -507,3 +507,144 @@
.sw-modal-hero{ height: 150px; }
.sw-modal-title{ font-size: 20px; line-height: 24px; }
}
+
+
+/* =========================================
+ SW_CENTERED_MODAL_ANIM
+ - fade + scale on open/close (slow)
+ ========================================= */
+.sw-centered-backdrop{
+ position: fixed;
+ inset: 0;
+ z-index: 9999999;
+ background: rgba(0,0,0,0.08);
+ backdrop-filter: blur(1px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 12px;
+ pointer-events: auto;
+
+ opacity: 0;
+ transition: opacity 440ms ease;
+}
+
+.sw-centered-modal{
+ transform: scale(0.94);
+ opacity: 0.0;
+ transition: transform 440ms ease, opacity 440ms ease;
+ will-change: transform, opacity;
+}
+
+.sw-centered-backdrop.sw-open{
+ opacity: 1;
+}
+
+.sw-centered-modal.sw-open{
+ transform: scale(1);
+ opacity: 1.0;
+}
+
+/* closing state (optional hook) */
+.sw-centered-backdrop.sw-closing{
+ opacity: 0;
+}
+
+
+/* =========================================
+ SW_MINI_SMOOTH_MOVE
+ - stop "sautillage/blink sec" while panning/zooming
+ - MapLibre animates marker transforms every frame.
+ - Any CSS transition/filter on marker subtree can cause shimmer/jitter on mobile.
+ ========================================= */
+.post-pin{
+ will-change: transform;
+ transform: translate3d(0,0,0);
+}
+
+/* NO transitions on marker subtree (prevents bounce/lag while map moves) */
+.post-pin, .post-pin *{
+ transition: none !important;
+}
+
+/* Make the mini template wrap GPU-friendly */
+.sw-template-mini-wrap{
+ backface-visibility: hidden;
+ transform: translate3d(-50%, calc(-100% - 12px), 0) scale(0.62);
+ transform-origin: bottom center;
+}
+
+/* Mobile scale keeps same behavior, but still GPU */
+@media (max-width: 640px){
+ .sw-template-mini-wrap{
+ transform: translate3d(-50%, calc(-100% - 12px), 0) scale(0.52);
+ }
+}
+
+/* Drop-shadow filters on triangles can shimmer on Android أثناء الحركة */
+.post-pin--compact .post-pin-pointer-small{
+ filter: none;
+}
+
+
+@media (prefers-reduced-motion: reduce){
+ .sw-centered-backdrop, .sw-centered-modal{
+ transition: none !important;
+ }
+}
+
+/* SW_MARKER_SMOOTH_APPEAR:BEGIN */
+.post-pin.sw-appear{
+ opacity: 0;
+ transform: translate3d(0,0,0) scale(0.96);
+ transition: opacity 840ms ease, transform 840ms ease;
+}
+.post-pin.sw-appear.sw-appear-in{
+ opacity: 1;
+ transform: translate3d(0,0,0) scale(1);
+}
+.post-pin.sw-disappear{
+ opacity: 0;
+ transform: translate3d(0,0,0) scale(0.96);
+ transition: opacity 840ms ease, transform 840ms ease;
+}
+/* SW_MARKER_SMOOTH_APPEAR:END */
+
+/* SW_CENTER_MODAL_ANIM:BEGIN */
+.sw-centered-backdrop{
+ opacity: 0;
+ transition: opacity 840ms ease;
+}
+.sw-centered-backdrop.sw-enter-active{ opacity: 1; }
+.sw-centered-backdrop.sw-leave{ opacity: 0; }
+
+.sw-centered-modal{
+ opacity: 0;
+ transform: translate3d(0,0,0) scale(0.92);
+ transition: opacity 840ms ease, transform 840ms cubic-bezier(.2,.9,.2,1);
+ will-change: opacity, transform;
+}
+.sw-centered-modal.sw-enter-active{
+ opacity: 1;
+ transform: translate3d(0,0,0) scale(1);
+}
+.sw-centered-modal.sw-leave{
+ opacity: 0;
+ transform: translate3d(0,0,0) scale(0.94);
+}
+/* SW_CENTER_MODAL_ANIM:END */
+
+/* SW_MODAL_FADE:BEGIN */
+.sw-centered-backdrop{ opacity:0; transition: opacity 840ms ease; }
+.sw-centered-backdrop.sw-enter-active{ opacity:1; }
+.sw-centered-backdrop.sw-leave{ opacity:0; }
+
+.sw-centered-modal{
+ opacity:0;
+ transform: translate3d(0,0,0) scale(0.92);
+ transition: opacity 840ms ease, transform 840ms cubic-bezier(.2,.9,.2,1);
+ will-change: opacity, transform;
+}
+.sw-centered-modal.sw-enter-active{ opacity:1; transform: translate3d(0,0,0) scale(1); }
+.sw-centered-modal.sw-leave{ opacity:0; transform: translate3d(0,0,0) scale(0.94); }
+/* SW_MODAL_FADE:END */
diff --git a/src/styles/mapMarkers.css.bak b/src/styles/mapMarkers.css.bak
new file mode 100644
index 0000000..461e309
--- /dev/null
+++ b/src/styles/mapMarkers.css.bak
@@ -0,0 +1,594 @@
+.post-pin {
+ position: relative;
+ width: 0;
+ height: 0;
+ pointer-events: none;
+ transform: translate3d(0, 0, 0);
+}
+.post-pin * { pointer-events: auto; box-sizing: border-box; }
+
+.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;
+ border-radius: 999px;
+ 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 { width:10px; height:10px; border-radius:999px; background:#ff8a00; margin-right:6px; flex-shrink:0; }
+.post-pin-text { color:#e8f5ff; font-size:11px; font-weight:500; max-width:160px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
+
+.post-pin--compact .post-pin-pointer-small {
+ position: absolute;
+ left: 0;
+ top: 0;
+ transform: translate(-50%, -100%);
+ width: 0;
+ height: 0;
+ 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));
+}
+
+/* ===== Template mini wrapper (keeps pointer unchanged) ===== */
+.sw-template-mini-wrap{
+ position: absolute;
+ left: 0;
+ top: 0;
+ transform: translate(-50%, calc(-100% - 12px)) scale(0.62);
+ transform-origin: bottom center;
+ will-change: transform;
+}
+
+@media (max-width: 640px){
+ .sw-template-mini-wrap{
+ transform: translate(-50%, calc(-100% - 12px)) scale(0.52);
+ }
+}
+
+/* =========================================================
+ NEW: FIXED EXPANDED OVERLAY
+ - Card is fixed near bottom (consistent)
+ - Pointer follows marker (consistent)
+ - No huge map recenter jumps
+ ========================================================= */
+
+.sw-expanded-overlay-root{
+ position:absolute;
+ inset:0;
+ z-index: 999999;
+ pointer-events:none;
+}
+
+/* Card position: "dans le bas" (approx bottom zone), consistent on rotate */
+.sw-expanded-overlay-card{
+ position:absolute;
+ left: 50%;
+ bottom: calc(12px + env(safe-area-inset-bottom) + var(--sw-below-overlap, 0px));
+ transform: translateX(-50%);
+ width: min(92vw, 420px);
+ pointer-events: auto;
+}
+
+/* Make card fit mobile nicely */
+.sw-expanded-overlay-card .post-card{
+ width: 100%;
+ max-height: min(58vh, 640px);
+ overflow:auto;
+ -webkit-overflow-scrolling: touch;
+ background: rgba(8, 20, 40, 0.97);
+ border-radius: 14px;
+ padding: 10px 12px;
+ box-shadow: 0 10px 26px rgba(0, 0, 0, 0.85);
+ border: 1px solid rgba(90, 190, 255, 0.9);
+ color: #e8f5ff;
+}
+
+/* Pointer on the marker position */
+.sw-overlay-pointer{
+ position:absolute;
+ width:0; height:0;
+ 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,.8));
+ transform: translate(-50%, -100%);
+ left: 0;
+ top: 0;
+}
+
+/* Simple line from marker to card */
+.sw-overlay-line{
+ position:absolute;
+ height: 2px;
+ background: rgba(56,189,248,0.35);
+ transform-origin: 0 50%;
+ left: 0;
+ top: 0;
+ width: 0;
+ filter: drop-shadow(0 1px 2px rgba(0,0,0,.6));
+}
+
+/* ===== Expanded layout pieces (same as before) ===== */
+.sw-expanded-shell{ padding: 10px 12px; }
+
+.sw-expanded-top{
+ display:flex;
+ gap:10px;
+ align-items:flex-start;
+}
+
+.sw-expanded-left{ flex: 1; }
+
+.sw-expanded-right{
+ width: 150px;
+ min-width: 140px;
+ display:flex;
+ flex-direction:column;
+ gap:8px;
+ padding-top: 4px;
+}
+
+.sw-watch-title{
+ font-size: 11px;
+ font-weight: 800;
+ color:#bfdbfe;
+ text-transform: uppercase;
+ letter-spacing: .06em;
+}
+
+.sw-watch-list{ display:flex; flex-direction:column; gap:6px; }
+
+.sw-watch-item{
+ font-size: 11px;
+ color:#e8f5ff;
+ background: rgba(3, 14, 32, 0.75);
+ border: 1px solid rgba(90, 190, 255, 0.35);
+ border-radius: 10px;
+ padding: 6px 8px;
+}
+
+.sw-watch-msg{
+ opacity:.85;
+ margin-top:2px;
+ font-size: 10px;
+ color:#c7e3ff;
+}
+
+.sw-add-feed-btn{
+ margin-top: 2px;
+ border: 1px solid rgba(148,163,184,.7);
+ border-radius: 10px;
+ padding: 8px 10px;
+ background: rgba(5, 35, 70, 0.85);
+ color:#e8f5ff;
+ font-weight: 900;
+ font-size: 11px;
+ cursor:pointer;
+}
+
+.sw-news-generated{
+ margin-top: 10px;
+ padding: 8px 10px;
+ border-radius: 12px;
+ background: rgba(3, 14, 32, 0.75);
+ border: 1px solid rgba(90, 190, 255, 0.35);
+}
+
+.sw-news-title{
+ font-size: 12px;
+ font-weight: 900;
+ color:#e8f5ff;
+ margin-bottom: 2px;
+}
+
+.sw-news-sub{ font-size: 10px; color:#9eb7ff; }
+
+.sw-actions-row{ margin-top: 8px; }
+
+.sw-livechat{ margin-top: 8px; }
+
+.sw-livechat-title{
+ font-weight: 800;
+ letter-spacing:.04em;
+ margin-bottom: 6px;
+}
+
+.sw-livechat-body{
+ display:flex;
+ flex-direction:column;
+ gap:3px;
+ margin-bottom: 8px;
+ opacity:.95;
+}
+
+.sw-chat-line{ font-size: 10px; color:#c7e3ff; }
+
+.sw-livechat-inputrow{ display:flex; gap:8px; align-items:center; }
+
+.sw-livechat-input{
+ flex:1;
+ border-radius: 999px;
+ border: 1px solid rgba(90, 190, 255, 0.35);
+ background: rgba(3, 14, 32, 0.75);
+ color:#e8f5ff;
+ padding: 8px 10px;
+ font-size: 11px;
+ outline: none;
+}
+
+.sw-livechat-post{
+ border:none;
+ border-radius: 10px;
+ padding: 8px 12px;
+ background: rgba(56,189,248,0.22);
+ color:#e8f5ff;
+ font-weight: 900;
+ font-size: 11px;
+ cursor:pointer;
+}
+
+/* Mobile: keep overlay readable */
+@media (max-width: 640px){
+ .sw-expanded-overlay-card{
+ width: min(94vw, 360px);
+ }
+ .sw-expanded-top{
+ flex-direction: column;
+ }
+ .sw-expanded-right{
+ 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; }
+}
+
+
+/* =========================================
+ SW_CENTERED_MODAL_ANIM
+ - fade + scale on open/close (slow)
+ ========================================= */
+.sw-centered-backdrop{
+ position: fixed;
+ inset: 0;
+ z-index: 9999999;
+ background: rgba(0,0,0,0.08);
+ backdrop-filter: blur(1px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 12px;
+ pointer-events: auto;
+
+ opacity: 0;
+ transition: opacity 440ms ease;
+}
+
+.sw-centered-modal{
+ transform: scale(0.94);
+ opacity: 0.0;
+ transition: transform 440ms ease, opacity 440ms ease;
+ will-change: transform, opacity;
+}
+
+.sw-centered-backdrop.sw-open{
+ opacity: 1;
+}
+
+.sw-centered-modal.sw-open{
+ transform: scale(1);
+ opacity: 1.0;
+}
+
+/* closing state (optional hook) */
+.sw-centered-backdrop.sw-closing{
+ opacity: 0;
+}
+
+
+/* =========================================
+ SW_MINI_SMOOTH_MOVE
+ - stop "sautillage/blink sec" while panning/zooming
+ - MapLibre animates marker transforms every frame.
+ - Any CSS transition/filter on marker subtree can cause shimmer/jitter on mobile.
+ ========================================= */
+.post-pin{
+ will-change: transform;
+ transform: translate3d(0,0,0);
+}
+
+/* NO transitions on marker subtree (prevents bounce/lag while map moves) */
+.post-pin, .post-pin *{
+ transition: none !important;
+}
+
+/* Make the mini template wrap GPU-friendly */
+.sw-template-mini-wrap{
+ backface-visibility: hidden;
+ transform: translate3d(-50%, calc(-100% - 12px), 0) scale(0.62);
+ transform-origin: bottom center;
+}
+
+/* Mobile scale keeps same behavior, but still GPU */
+@media (max-width: 640px){
+ .sw-template-mini-wrap{
+ transform: translate3d(-50%, calc(-100% - 12px), 0) scale(0.52);
+ }
+}
+
+/* Drop-shadow filters on triangles can shimmer on Android أثناء الحركة */
+.post-pin--compact .post-pin-pointer-small{
+ filter: none;
+}
+
+
+@media (prefers-reduced-motion: reduce){
+ .sw-centered-backdrop, .sw-centered-modal{
+ transition: none !important;
+ }
+}
diff --git a/src/styles/mapMarkers.css.bak2 b/src/styles/mapMarkers.css.bak2
new file mode 100644
index 0000000..dab1752
--- /dev/null
+++ b/src/styles/mapMarkers.css.bak2
@@ -0,0 +1,604 @@
+.post-pin {
+ position: relative;
+ width: 0;
+ height: 0;
+ pointer-events: none;
+ transform: translate3d(0, 0, 0);
+}
+.post-pin * { pointer-events: auto; box-sizing: border-box; }
+
+.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;
+ border-radius: 999px;
+ 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 { width:10px; height:10px; border-radius:999px; background:#ff8a00; margin-right:6px; flex-shrink:0; }
+.post-pin-text { color:#e8f5ff; font-size:11px; font-weight:500; max-width:160px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
+
+.post-pin--compact .post-pin-pointer-small {
+ position: absolute;
+ left: 0;
+ top: 0;
+ transform: translate(-50%, -100%);
+ width: 0;
+ height: 0;
+ 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));
+}
+
+/* ===== Template mini wrapper (keeps pointer unchanged) ===== */
+.sw-template-mini-wrap{
+ position: absolute;
+ left: 0;
+ top: 0;
+ transform: translate(-50%, calc(-100% - 12px)) scale(0.62);
+ transform-origin: bottom center;
+ will-change: transform;
+}
+
+@media (max-width: 640px){
+ .sw-template-mini-wrap{
+ transform: translate(-50%, calc(-100% - 12px)) scale(0.52);
+ }
+}
+
+/* =========================================================
+ NEW: FIXED EXPANDED OVERLAY
+ - Card is fixed near bottom (consistent)
+ - Pointer follows marker (consistent)
+ - No huge map recenter jumps
+ ========================================================= */
+
+.sw-expanded-overlay-root{
+ position:absolute;
+ inset:0;
+ z-index: 999999;
+ pointer-events:none;
+}
+
+/* Card position: "dans le bas" (approx bottom zone), consistent on rotate */
+.sw-expanded-overlay-card{
+ position:absolute;
+ left: 50%;
+ bottom: calc(12px + env(safe-area-inset-bottom) + var(--sw-below-overlap, 0px));
+ transform: translateX(-50%);
+ width: min(92vw, 420px);
+ pointer-events: auto;
+}
+
+/* Make card fit mobile nicely */
+.sw-expanded-overlay-card .post-card{
+ width: 100%;
+ max-height: min(58vh, 640px);
+ overflow:auto;
+ -webkit-overflow-scrolling: touch;
+ background: rgba(8, 20, 40, 0.97);
+ border-radius: 14px;
+ padding: 10px 12px;
+ box-shadow: 0 10px 26px rgba(0, 0, 0, 0.85);
+ border: 1px solid rgba(90, 190, 255, 0.9);
+ color: #e8f5ff;
+}
+
+/* Pointer on the marker position */
+.sw-overlay-pointer{
+ position:absolute;
+ width:0; height:0;
+ 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,.8));
+ transform: translate(-50%, -100%);
+ left: 0;
+ top: 0;
+}
+
+/* Simple line from marker to card */
+.sw-overlay-line{
+ position:absolute;
+ height: 2px;
+ background: rgba(56,189,248,0.35);
+ transform-origin: 0 50%;
+ left: 0;
+ top: 0;
+ width: 0;
+ filter: drop-shadow(0 1px 2px rgba(0,0,0,.6));
+}
+
+/* ===== Expanded layout pieces (same as before) ===== */
+.sw-expanded-shell{ padding: 10px 12px; }
+
+.sw-expanded-top{
+ display:flex;
+ gap:10px;
+ align-items:flex-start;
+}
+
+.sw-expanded-left{ flex: 1; }
+
+.sw-expanded-right{
+ width: 150px;
+ min-width: 140px;
+ display:flex;
+ flex-direction:column;
+ gap:8px;
+ padding-top: 4px;
+}
+
+.sw-watch-title{
+ font-size: 11px;
+ font-weight: 800;
+ color:#bfdbfe;
+ text-transform: uppercase;
+ letter-spacing: .06em;
+}
+
+.sw-watch-list{ display:flex; flex-direction:column; gap:6px; }
+
+.sw-watch-item{
+ font-size: 11px;
+ color:#e8f5ff;
+ background: rgba(3, 14, 32, 0.75);
+ border: 1px solid rgba(90, 190, 255, 0.35);
+ border-radius: 10px;
+ padding: 6px 8px;
+}
+
+.sw-watch-msg{
+ opacity:.85;
+ margin-top:2px;
+ font-size: 10px;
+ color:#c7e3ff;
+}
+
+.sw-add-feed-btn{
+ margin-top: 2px;
+ border: 1px solid rgba(148,163,184,.7);
+ border-radius: 10px;
+ padding: 8px 10px;
+ background: rgba(5, 35, 70, 0.85);
+ color:#e8f5ff;
+ font-weight: 900;
+ font-size: 11px;
+ cursor:pointer;
+}
+
+.sw-news-generated{
+ margin-top: 10px;
+ padding: 8px 10px;
+ border-radius: 12px;
+ background: rgba(3, 14, 32, 0.75);
+ border: 1px solid rgba(90, 190, 255, 0.35);
+}
+
+.sw-news-title{
+ font-size: 12px;
+ font-weight: 900;
+ color:#e8f5ff;
+ margin-bottom: 2px;
+}
+
+.sw-news-sub{ font-size: 10px; color:#9eb7ff; }
+
+.sw-actions-row{ margin-top: 8px; }
+
+.sw-livechat{ margin-top: 8px; }
+
+.sw-livechat-title{
+ font-weight: 800;
+ letter-spacing:.04em;
+ margin-bottom: 6px;
+}
+
+.sw-livechat-body{
+ display:flex;
+ flex-direction:column;
+ gap:3px;
+ margin-bottom: 8px;
+ opacity:.95;
+}
+
+.sw-chat-line{ font-size: 10px; color:#c7e3ff; }
+
+.sw-livechat-inputrow{ display:flex; gap:8px; align-items:center; }
+
+.sw-livechat-input{
+ flex:1;
+ border-radius: 999px;
+ border: 1px solid rgba(90, 190, 255, 0.35);
+ background: rgba(3, 14, 32, 0.75);
+ color:#e8f5ff;
+ padding: 8px 10px;
+ font-size: 11px;
+ outline: none;
+}
+
+.sw-livechat-post{
+ border:none;
+ border-radius: 10px;
+ padding: 8px 12px;
+ background: rgba(56,189,248,0.22);
+ color:#e8f5ff;
+ font-weight: 900;
+ font-size: 11px;
+ cursor:pointer;
+}
+
+/* Mobile: keep overlay readable */
+@media (max-width: 640px){
+ .sw-expanded-overlay-card{
+ width: min(94vw, 360px);
+ }
+ .sw-expanded-top{
+ flex-direction: column;
+ }
+ .sw-expanded-right{
+ 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; }
+}
+
+
+/* =========================================
+ SW_CENTERED_MODAL_ANIM
+ - fade + scale on open/close (slow)
+ ========================================= */
+.sw-centered-backdrop{
+ position: fixed;
+ inset: 0;
+ z-index: 9999999;
+ background: rgba(0,0,0,0.08);
+ backdrop-filter: blur(1px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 12px;
+ pointer-events: auto;
+
+ opacity: 0;
+ transition: opacity 440ms ease;
+}
+
+.sw-centered-modal{
+ transform: scale(0.94);
+ opacity: 0.0;
+ transition: transform 440ms ease, opacity 440ms ease;
+ will-change: transform, opacity;
+}
+
+.sw-centered-backdrop.sw-open{
+ opacity: 1;
+}
+
+.sw-centered-modal.sw-open{
+ transform: scale(1);
+ opacity: 1.0;
+}
+
+/* closing state (optional hook) */
+.sw-centered-backdrop.sw-closing{
+ opacity: 0;
+}
+
+
+/* =========================================
+ SW_MINI_SMOOTH_MOVE
+ - stop "sautillage/blink sec" while panning/zooming
+ - MapLibre animates marker transforms every frame.
+ - Any CSS transition/filter on marker subtree can cause shimmer/jitter on mobile.
+ ========================================= */
+.post-pin{
+ will-change: transform;
+ transform: translate3d(0,0,0);
+}
+
+/* NO transitions on marker subtree (prevents bounce/lag while map moves) */
+.post-pin, .post-pin *{
+ transition: none !important;
+}
+
+/* Make the mini template wrap GPU-friendly */
+.sw-template-mini-wrap{
+ backface-visibility: hidden;
+ transform: translate3d(-50%, calc(-100% - 12px), 0) scale(0.62);
+ transform-origin: bottom center;
+}
+
+/* Mobile scale keeps same behavior, but still GPU */
+@media (max-width: 640px){
+ .sw-template-mini-wrap{
+ transform: translate3d(-50%, calc(-100% - 12px), 0) scale(0.52);
+ }
+}
+
+/* Drop-shadow filters on triangles can shimmer on Android أثناء الحركة */
+.post-pin--compact .post-pin-pointer-small{
+ filter: none;
+}
+
+
+@media (prefers-reduced-motion: reduce){
+ .sw-centered-backdrop, .sw-centered-modal{
+ transition: none !important;
+ }
+}
+
+/* SW_MARKER_SMOOTH_APPEAR:BEGIN */
+.post-pin.sw-appear{
+ opacity: 0;
+ transition: opacity 280ms ease;
+}
+.post-pin.sw-appear.sw-appear-in{
+ opacity: 1;
+}
+/* SW_MARKER_SMOOTH_APPEAR:END */
diff --git a/src/styles/mapMarkers.css.bak3 b/src/styles/mapMarkers.css.bak3
new file mode 100644
index 0000000..06fa9d4
--- /dev/null
+++ b/src/styles/mapMarkers.css.bak3
@@ -0,0 +1,608 @@
+.post-pin {
+ position: relative;
+ width: 0;
+ height: 0;
+ pointer-events: none;
+ transform: translate3d(0, 0, 0);
+}
+.post-pin * { pointer-events: auto; box-sizing: border-box; }
+
+.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;
+ border-radius: 999px;
+ 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 { width:10px; height:10px; border-radius:999px; background:#ff8a00; margin-right:6px; flex-shrink:0; }
+.post-pin-text { color:#e8f5ff; font-size:11px; font-weight:500; max-width:160px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
+
+.post-pin--compact .post-pin-pointer-small {
+ position: absolute;
+ left: 0;
+ top: 0;
+ transform: translate(-50%, -100%);
+ width: 0;
+ height: 0;
+ 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));
+}
+
+/* ===== Template mini wrapper (keeps pointer unchanged) ===== */
+.sw-template-mini-wrap{
+ position: absolute;
+ left: 0;
+ top: 0;
+ transform: translate(-50%, calc(-100% - 12px)) scale(0.62);
+ transform-origin: bottom center;
+ will-change: transform;
+}
+
+@media (max-width: 640px){
+ .sw-template-mini-wrap{
+ transform: translate(-50%, calc(-100% - 12px)) scale(0.52);
+ }
+}
+
+/* =========================================================
+ NEW: FIXED EXPANDED OVERLAY
+ - Card is fixed near bottom (consistent)
+ - Pointer follows marker (consistent)
+ - No huge map recenter jumps
+ ========================================================= */
+
+.sw-expanded-overlay-root{
+ position:absolute;
+ inset:0;
+ z-index: 999999;
+ pointer-events:none;
+}
+
+/* Card position: "dans le bas" (approx bottom zone), consistent on rotate */
+.sw-expanded-overlay-card{
+ position:absolute;
+ left: 50%;
+ bottom: calc(12px + env(safe-area-inset-bottom) + var(--sw-below-overlap, 0px));
+ transform: translateX(-50%);
+ width: min(92vw, 420px);
+ pointer-events: auto;
+}
+
+/* Make card fit mobile nicely */
+.sw-expanded-overlay-card .post-card{
+ width: 100%;
+ max-height: min(58vh, 640px);
+ overflow:auto;
+ -webkit-overflow-scrolling: touch;
+ background: rgba(8, 20, 40, 0.97);
+ border-radius: 14px;
+ padding: 10px 12px;
+ box-shadow: 0 10px 26px rgba(0, 0, 0, 0.85);
+ border: 1px solid rgba(90, 190, 255, 0.9);
+ color: #e8f5ff;
+}
+
+/* Pointer on the marker position */
+.sw-overlay-pointer{
+ position:absolute;
+ width:0; height:0;
+ 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,.8));
+ transform: translate(-50%, -100%);
+ left: 0;
+ top: 0;
+}
+
+/* Simple line from marker to card */
+.sw-overlay-line{
+ position:absolute;
+ height: 2px;
+ background: rgba(56,189,248,0.35);
+ transform-origin: 0 50%;
+ left: 0;
+ top: 0;
+ width: 0;
+ filter: drop-shadow(0 1px 2px rgba(0,0,0,.6));
+}
+
+/* ===== Expanded layout pieces (same as before) ===== */
+.sw-expanded-shell{ padding: 10px 12px; }
+
+.sw-expanded-top{
+ display:flex;
+ gap:10px;
+ align-items:flex-start;
+}
+
+.sw-expanded-left{ flex: 1; }
+
+.sw-expanded-right{
+ width: 150px;
+ min-width: 140px;
+ display:flex;
+ flex-direction:column;
+ gap:8px;
+ padding-top: 4px;
+}
+
+.sw-watch-title{
+ font-size: 11px;
+ font-weight: 800;
+ color:#bfdbfe;
+ text-transform: uppercase;
+ letter-spacing: .06em;
+}
+
+.sw-watch-list{ display:flex; flex-direction:column; gap:6px; }
+
+.sw-watch-item{
+ font-size: 11px;
+ color:#e8f5ff;
+ background: rgba(3, 14, 32, 0.75);
+ border: 1px solid rgba(90, 190, 255, 0.35);
+ border-radius: 10px;
+ padding: 6px 8px;
+}
+
+.sw-watch-msg{
+ opacity:.85;
+ margin-top:2px;
+ font-size: 10px;
+ color:#c7e3ff;
+}
+
+.sw-add-feed-btn{
+ margin-top: 2px;
+ border: 1px solid rgba(148,163,184,.7);
+ border-radius: 10px;
+ padding: 8px 10px;
+ background: rgba(5, 35, 70, 0.85);
+ color:#e8f5ff;
+ font-weight: 900;
+ font-size: 11px;
+ cursor:pointer;
+}
+
+.sw-news-generated{
+ margin-top: 10px;
+ padding: 8px 10px;
+ border-radius: 12px;
+ background: rgba(3, 14, 32, 0.75);
+ border: 1px solid rgba(90, 190, 255, 0.35);
+}
+
+.sw-news-title{
+ font-size: 12px;
+ font-weight: 900;
+ color:#e8f5ff;
+ margin-bottom: 2px;
+}
+
+.sw-news-sub{ font-size: 10px; color:#9eb7ff; }
+
+.sw-actions-row{ margin-top: 8px; }
+
+.sw-livechat{ margin-top: 8px; }
+
+.sw-livechat-title{
+ font-weight: 800;
+ letter-spacing:.04em;
+ margin-bottom: 6px;
+}
+
+.sw-livechat-body{
+ display:flex;
+ flex-direction:column;
+ gap:3px;
+ margin-bottom: 8px;
+ opacity:.95;
+}
+
+.sw-chat-line{ font-size: 10px; color:#c7e3ff; }
+
+.sw-livechat-inputrow{ display:flex; gap:8px; align-items:center; }
+
+.sw-livechat-input{
+ flex:1;
+ border-radius: 999px;
+ border: 1px solid rgba(90, 190, 255, 0.35);
+ background: rgba(3, 14, 32, 0.75);
+ color:#e8f5ff;
+ padding: 8px 10px;
+ font-size: 11px;
+ outline: none;
+}
+
+.sw-livechat-post{
+ border:none;
+ border-radius: 10px;
+ padding: 8px 12px;
+ background: rgba(56,189,248,0.22);
+ color:#e8f5ff;
+ font-weight: 900;
+ font-size: 11px;
+ cursor:pointer;
+}
+
+/* Mobile: keep overlay readable */
+@media (max-width: 640px){
+ .sw-expanded-overlay-card{
+ width: min(94vw, 360px);
+ }
+ .sw-expanded-top{
+ flex-direction: column;
+ }
+ .sw-expanded-right{
+ 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; }
+}
+
+
+/* =========================================
+ SW_CENTERED_MODAL_ANIM
+ - fade + scale on open/close (slow)
+ ========================================= */
+.sw-centered-backdrop{
+ position: fixed;
+ inset: 0;
+ z-index: 9999999;
+ background: rgba(0,0,0,0.08);
+ backdrop-filter: blur(1px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 12px;
+ pointer-events: auto;
+
+ opacity: 0;
+ transition: opacity 440ms ease;
+}
+
+.sw-centered-modal{
+ transform: scale(0.94);
+ opacity: 0.0;
+ transition: transform 440ms ease, opacity 440ms ease;
+ will-change: transform, opacity;
+}
+
+.sw-centered-backdrop.sw-open{
+ opacity: 1;
+}
+
+.sw-centered-modal.sw-open{
+ transform: scale(1);
+ opacity: 1.0;
+}
+
+/* closing state (optional hook) */
+.sw-centered-backdrop.sw-closing{
+ opacity: 0;
+}
+
+
+/* =========================================
+ SW_MINI_SMOOTH_MOVE
+ - stop "sautillage/blink sec" while panning/zooming
+ - MapLibre animates marker transforms every frame.
+ - Any CSS transition/filter on marker subtree can cause shimmer/jitter on mobile.
+ ========================================= */
+.post-pin{
+ will-change: transform;
+ transform: translate3d(0,0,0);
+}
+
+/* NO transitions on marker subtree (prevents bounce/lag while map moves) */
+.post-pin, .post-pin *{
+ transition: none !important;
+}
+
+/* Make the mini template wrap GPU-friendly */
+.sw-template-mini-wrap{
+ backface-visibility: hidden;
+ transform: translate3d(-50%, calc(-100% - 12px), 0) scale(0.62);
+ transform-origin: bottom center;
+}
+
+/* Mobile scale keeps same behavior, but still GPU */
+@media (max-width: 640px){
+ .sw-template-mini-wrap{
+ transform: translate3d(-50%, calc(-100% - 12px), 0) scale(0.52);
+ }
+}
+
+/* Drop-shadow filters on triangles can shimmer on Android أثناء الحركة */
+.post-pin--compact .post-pin-pointer-small{
+ filter: none;
+}
+
+
+@media (prefers-reduced-motion: reduce){
+ .sw-centered-backdrop, .sw-centered-modal{
+ transition: none !important;
+ }
+}
+
+/* SW_MARKER_SMOOTH_APPEAR:BEGIN */
+.post-pin.sw-appear{
+ opacity: 0;
+ transition: opacity 280ms ease;
+}
+.post-pin.sw-appear.sw-appear-in{
+ opacity: 1;
+}
+.post-pin.sw-disappear{
+ opacity: 0;
+ transition: opacity 280ms ease;
+}
+/* SW_MARKER_SMOOTH_APPEAR:END */
diff --git a/src/styles/posts.css b/src/styles/posts.css
index cfc9930..4c9bc4c 100644
--- a/src/styles/posts.css
+++ b/src/styles/posts.css
@@ -135,3 +135,28 @@
}
/* SW_LAYOUT_RATIO_FIX:END */
+/* SW_DESKTOP_WIDE_LAYOUT:BEGIN */
+/* Big browser: Sociowall 75% / Chat 25% + centered container */
+@media (min-width: 900px){
+ .below-stage{
+ max-width: 1320px;
+ margin-left: auto;
+ margin-right: auto;
+ padding-left: 18px;
+ padding-right: 18px;
+ }
+
+ .below-row{ gap: 0.9rem; }
+
+ .sociowall-panel{ flex: 0 0 74%; }
+ .chat-panel{
+ flex: 0 0 26%;
+ width: auto;
+ max-width: none;
+ min-width: 260px;
+ }
+
+ .post-list{ max-height: 50vh; }
+ .chat-users{ max-height: 50vh; }
+}
+/* SW_DESKTOP_WIDE_LAYOUT:END */
diff --git a/src/styles/posts.css.bak b/src/styles/posts.css.bak
new file mode 100644
index 0000000..cfc9930
--- /dev/null
+++ b/src/styles/posts.css.bak
@@ -0,0 +1,137 @@
+/* Sociowall + Chat BELOW the map */
+.below-row{
+ display:flex;
+ gap:.6rem;
+ align-items:stretch;
+ min-width:0;
+ flex-wrap: nowrap; /* ✅ keep side-by-side */
+}
+
+/* Small top nudge down (as requested) */
+.sociowall-panel,
+.chat-panel{
+ max-width: 32vw;
+ min-width: 160px;
+ flex: 1 1 0; /* ✅ 1/4 */
+ margin-top: 5px; /* ✅ descend 5px */
+}
+
+.sociowall-panel{
+ flex: 3 1 0; /* ✅ 3/4 */
+ min-width:0; /* IMPORTANT: prevents pushing chat */
+ min-height: 160px;
+ background: rgba(15, 23, 42, 0.96);
+ border-radius: 14px;
+ border: 1px solid rgba(148, 163, 184, 0.9);
+ box-shadow: 0 10px 28px rgba(0,0,0,.9), 0 0 18px rgba(56,189,248,.45);
+ padding: .45rem .55rem;
+ display:flex;
+ flex-direction:column;
+}
+
+.sociowall-header{
+ font-size: .78rem;
+ font-weight: 700;
+ letter-spacing: .06em;
+ text-transform: uppercase;
+ color:#bfdbfe;
+ margin-bottom:.25rem;
+}
+
+/* list scroll inside wall */
+.post-list{
+ flex:1;
+ overflow-y:auto;
+ padding-right:.15rem;
+ max-height: 44vh;
+}
+
+/* prevent long content from widening layout */
+.post-card{
+ overflow:hidden;
+ min-width:0;
+ border-radius:10px;
+ border:1px solid rgba(55, 65, 81, 0.9);
+ background: rgba(15, 23, 42, 0.92);
+ padding:.3rem .45rem;
+ margin-bottom:.22rem;
+ cursor:pointer;
+}
+
+/* SCALE the fixed template so it never forces width */
+.post-card .sw-wall-mini{
+ --sw-wall-scale: 0.86;
+ transform: scale(var(--sw-wall-scale));
+ transform-origin: left top;
+ width: calc(100% / var(--sw-wall-scale));
+}
+
+.post-list-header{ display:flex; align-items:center; gap:.5rem; margin-bottom:.25rem; font-size:.7rem; color:#cbd5f5; }
+.km-filter input[type="range"]{ width:160px; }
+
+.status-text{ font-size:.72rem; opacity:.85; margin:.15rem 0; }
+.status-error{ color:#fecaca; }
+
+/* ✅ Chat fixed column that can shrink a bit on narrow screens */
+.chat-panel{
+ flex: 0 0 190px;
+ width: 190px;
+ min-width: 160px; /* allow shrink in portrait */
+ max-width: min(220px, 42vw);
+ min-height: 160px;
+ background: rgba(15, 23, 42, 0.96);
+ border-radius: 14px;
+ border: 1px solid rgba(148, 163, 184, 0.9);
+ box-shadow: 0 10px 28px rgba(0,0,0,.9), 0 0 18px rgba(56,189,248,.45);
+ padding:.4rem .45rem;
+ display:flex;
+ flex-direction:column;
+ overflow:hidden;
+}
+
+.chat-header{
+ font-size:.78rem;
+ font-weight:700;
+ letter-spacing:.06em;
+ text-transform:uppercase;
+ color:#bfdbfe;
+ margin-bottom:.2rem;
+}
+
+.chat-users{
+ list-style:none;
+ padding:0; margin:0;
+ font-size:.76rem;
+ overflow:auto;
+ -webkit-overflow-scrolling: touch;
+ max-height: 44vh;
+}
+
+.chat-users li{ display:flex; align-items:center; gap:.3rem; padding:.18rem .1rem; }
+.chat-users li::before{ content:"👤"; font-size:.72rem; opacity:.9; }
+
+/* Stack only ultra-ultra-small */
+@media (max-width: 340px){
+ .below-row{ flex-direction: column; }
+ .chat-panel{ width:100%; min-width:0; max-width:none; flex: 1 1 auto; }
+ .post-list{ max-height: 46vh; }
+}
+
+/* SW_LAYOUT_RATIO_FIX:BEGIN */
+.below-row{ flex-wrap: nowrap !important; }
+
+.sociowall-panel{
+ flex: 3 1 0 !important; /* ✅ 3/4 */
+ width: auto !important;
+ max-width: none !important;
+ min-width: 0 !important;
+}
+
+.chat-panel{
+ flex: 1 1 0 !important; /* ✅ 1/4 */
+ width: auto !important;
+ max-width: none !important;
+ min-width: 140px !important;
+}
+/* SW_LAYOUT_RATIO_FIX:END */
+
diff --git a/src/styles/topbar.css b/src/styles/topbar.css
index 979e0d1..aa73bf4 100644
--- a/src/styles/topbar.css
+++ b/src/styles/topbar.css
@@ -29,7 +29,7 @@
border:1px solid rgba(148,163,184,.85);
background:rgba(15,23,42,.9); color:#e5e7eb; font-size:.65rem;
display:flex; align-items:center; justify-content:center; padding:0; cursor:pointer;
- box-shadow:0 0 6px rgba(15,23,42,.9); transition:transform .12s ease;
+ box-shadow:0 0 6px rgba(15,23,42,.9); transition:transform 0.36s ease;
}
.theme-dot:hover{ transform:scale(1.06); }
.theme-dot-active{ border-color:#facc15; box-shadow:0 0 10px rgba(250,204,21,.9),0 0 14px rgba(56,189,248,.55); }
diff --git a/src/styles/topbar.css.bak3 b/src/styles/topbar.css.bak3
new file mode 100644
index 0000000..979e0d1
--- /dev/null
+++ b/src/styles/topbar.css.bak3
@@ -0,0 +1,61 @@
+.topbar{
+ position: sticky; top:0; z-index:100;
+ width:100%;
+ padding: .55rem .9rem;
+ display:flex; align-items:center; justify-content:space-between; gap:.6rem;
+ background: linear-gradient(135deg,#071225 0%,#0b2b5f 38%,#0e65c0 72%,#27a8ff 100%);
+ border-bottom-left-radius:18px; border-bottom-right-radius:18px;
+ box-shadow: 0 10px 24px rgba(0,0,0,.55), 0 0 18px rgba(56,189,248,.45);
+ overflow:hidden;
+}
+
+.logo-circle{
+ width:40px; height:40px; border-radius:50%; flex-shrink:0;
+ background-image:url("/icons/logo-master.png");
+ background-size:cover; background-position:center; background-repeat:no-repeat;
+ box-shadow: 0 0 10px rgba(37,99,235,.8), 0 0 18px rgba(56,189,248,.6);
+ color:transparent;
+}
+
+.title-block{ display:flex; flex-direction:column; line-height:1.05; min-width:0; }
+.main-title{ font-size:1.02rem; font-weight:800; letter-spacing:.04em; color:#f9fafb; }
+.sub-title{ font-size:.74rem; color:#dbeafe; opacity:.95; }
+
+.topbar-right{ display:flex; align-items:center; gap:.5rem; margin-left:auto; flex-wrap:wrap; }
+
+.theme-switch{ display:flex; gap:.28rem; }
+.theme-dot{
+ width:22px; height:22px; border-radius:999px;
+ border:1px solid rgba(148,163,184,.85);
+ background:rgba(15,23,42,.9); color:#e5e7eb; font-size:.65rem;
+ display:flex; align-items:center; justify-content:center; padding:0; cursor:pointer;
+ box-shadow:0 0 6px rgba(15,23,42,.9); transition:transform .12s ease;
+}
+.theme-dot:hover{ transform:scale(1.06); }
+.theme-dot-active{ border-color:#facc15; box-shadow:0 0 10px rgba(250,204,21,.9),0 0 14px rgba(56,189,248,.55); }
+
+.btn-primary, .btn-secondary{
+ padding:.32rem .85rem; font-size:.78rem; border-radius:999px; font-weight:700; cursor:pointer;
+ white-space:nowrap; line-height:1;
+}
+.btn-primary{
+ border:1px solid #22c55e;
+ background:radial-gradient(circle at 30% 30%,#22c55e,#16a34a);
+ color:#f9fafb;
+ box-shadow:0 4px 10px rgba(22,163,74,.5),0 0 8px rgba(22,163,74,.6);
+}
+.btn-primary:disabled{ opacity:.6; cursor:default; }
+.btn-secondary{
+ border:1px solid #bfdbfe;
+ background:rgba(15,23,42,.25);
+ color:#e0f2fe;
+ box-shadow:0 3px 10px rgba(15,23,42,.6);
+}
+.btn-secondary:hover{ background:rgba(15,23,42,.45); }
+
+@media (max-width:640px){
+ .topbar{ padding:.5rem .7rem; border-bottom-left-radius:16px; border-bottom-right-radius:16px; }
+ .main-title{ font-size:1rem; }
+ .sub-title{ font-size:.7rem; }
+ .topbar-right{ width:100%; justify-content:flex-start; gap:.45rem; }
+}
diff --git a/src/theme/cardTokens.js b/src/theme/cardTokens.js
index 043149c..66f152e 100644
--- a/src/theme/cardTokens.js
+++ b/src/theme/cardTokens.js
@@ -9,7 +9,12 @@ export const cardTokens = {
},
gradients: {
newsBlue: "linear-gradient(135deg, rgba(78,161,255,0.35), rgba(16,18,24,1))",
- },
+ breakingRed: "linear-gradient(135deg, rgba(255,77,77,0.28), rgba(16,18,24,1))",
+ marketGold: "linear-gradient(135deg, rgba(250,204,21,0.22), rgba(16,18,24,1))",
+ friendsGreen: "linear-gradient(135deg, rgba(34,197,94,0.22), rgba(16,18,24,1))",
+ eventsPurple: "linear-gradient(135deg, rgba(168,85,247,0.22), rgba(16,18,24,1))",
+ financeTeal: "linear-gradient(135deg, rgba(45,212,191,0.22), rgba(16,18,24,1))",
+},
radius: { card: 18 },
shadow: { card: "0 10px 30px rgba(0,0,0,0.35)" },
text: {
@@ -37,7 +42,12 @@ export const cardTokens = {
},
gradients: {
newsBlue: "linear-gradient(135deg, rgba(56,189,248,0.35), rgba(7,18,37,1))",
- },
+ breakingRed: "linear-gradient(135deg, rgba(255,77,77,0.28), rgba(7,18,37,1))",
+ marketGold: "linear-gradient(135deg, rgba(250,204,21,0.22), rgba(7,18,37,1))",
+ friendsGreen: "linear-gradient(135deg, rgba(34,197,94,0.22), rgba(7,18,37,1))",
+ eventsPurple: "linear-gradient(135deg, rgba(168,85,247,0.22), rgba(7,18,37,1))",
+ financeTeal: "linear-gradient(135deg, rgba(45,212,191,0.22), rgba(7,18,37,1))",
+},
radius: { card: 18 },
shadow: { card: "0 10px 30px rgba(0,0,0,0.40)" },
text: {
@@ -65,7 +75,12 @@ export const cardTokens = {
},
gradients: {
newsBlue: "linear-gradient(135deg, rgba(30,107,255,0.20), rgba(255,255,255,1))",
- },
+ breakingRed: "linear-gradient(135deg, rgba(209,0,31,0.16), rgba(255,255,255,1))",
+ marketGold: "linear-gradient(135deg, rgba(250,204,21,0.18), rgba(255,255,255,1))",
+ friendsGreen: "linear-gradient(135deg, rgba(34,197,94,0.16), rgba(255,255,255,1))",
+ eventsPurple: "linear-gradient(135deg, rgba(168,85,247,0.16), rgba(255,255,255,1))",
+ financeTeal: "linear-gradient(135deg, rgba(45,212,191,0.16), rgba(255,255,255,1))",
+},
radius: { card: 18 },
shadow: { card: "0 10px 24px rgba(0,0,0,0.12)" },
text: {