diff --git a/src/components/Map/MapView.jsx b/src/components/Map/MapView.jsx index f784933..3f51667 100644 --- a/src/components/Map/MapView.jsx +++ b/src/components/Map/MapView.jsx @@ -282,6 +282,8 @@ export default function MapView({ // Live broadcast state const [showLiveBroadcast, setShowLiveBroadcast] = useState(false); + const [showShareMenu, setShowShareMenu] = useState(false); + const [shareData, setShareData] = useState({ url: '', text: '' }); const [activeLivePost, setActiveLivePost] = useState(null); const [liveCoords, setLiveCoords] = useState(null); // Weather modal state @@ -740,6 +742,55 @@ export default function MapView({ }); }, [onSelectPost, openOverlayWhenReady]); + // Restore shared view from URL params (?lat=X&lon=Y&z=Z&cat=CAT&sub=SUB&time=H) + const sharedViewRestoredRef = useRef(false); + useEffect(() => { + if (sharedViewRestoredRef.current) return; + const qs = new URLSearchParams(window.location.search || ""); + const lat = parseFloat(qs.get("lat")); + const lon = parseFloat(qs.get("lon")); + const z = parseInt(qs.get("z"), 10); + const cat = qs.get("cat"); + const sub = qs.get("sub"); + const time = parseInt(qs.get("time"), 10); + + // Only restore if lat/lon are present + if (!Number.isFinite(lat) || !Number.isFinite(lon)) return; + + sharedViewRestoredRef.current = true; + skipAutoZoomRef.current = true; + + // Restore filters + if (cat) { + const cats = cat.split(",").filter(Boolean); + if (cats.length > 0) setMainFilters(cats); + } + if (sub) { + const subs = sub.split(",").filter(Boolean); + if (subs.length > 0) setSubFilters(subs); + } + if (Number.isFinite(time) && time > 0) { + setTimeHours(time); + } + + // Fly to location when map is ready + const flyWhenReady = () => { + const map = mapRef.current; + if (!map) { + setTimeout(flyWhenReady, 200); + return; + } + const zoom = Number.isFinite(z) ? z : 10; + map.flyTo({ center: [lon, lat], zoom, speed: 1.5 }); + }; + flyWhenReady(); + + // Clean URL + try { + window.history.replaceState(null, "", "/"); + } catch {} + }, []); + useEffect(() => { if (queryPostRef.current) return; if (!CLUSTERS_ENABLED) return; @@ -1984,6 +2035,37 @@ export default function MapView({ > + + + +
+ {/* Copy URL */} + + + {/* Copy as text */} + + + {/* Native share (mobile) */} + {typeof navigator !== 'undefined' && navigator.share && ( + + )} +
+ + {/* Preview */} +
+ {shareData.url} +
+ + + )} ); } diff --git a/src/components/Map/markerManager.js b/src/components/Map/markerManager.js index 9bd88a2..7407b1b 100644 --- a/src/components/Map/markerManager.js +++ b/src/components/Map/markerManager.js @@ -388,6 +388,71 @@ function mountCard(container, spec, data, theme, post) { } const tokens = cardTokens?.[theme] || cardTokens.blue; const score = truthScore(post || data || {}); + const templateKey = getTemplateKeyForPost(post || {}); + const isPolaroid = templateKey === 'polaroid'; + + // Build children array + const children = [ + React.createElement(CardRenderer, { + key: "card", + spec, + data, + themeTokens: tokens, + onAction: (action, value) => { + if (action?.type === "open_url" && value) { + try { window.open(value, "_blank"); } catch {} + } + }, + className: "", + }), + ]; + + // For polaroid: just a small round indicator at bottom right + // For others: full truth bar on the side + if (isPolaroid) { + children.push( + React.createElement( + "div", + { + key: "truth-dot", + className: "sw-truth-dot", + style: { + position: 'absolute', + bottom: 6, + right: 6, + width: 18, + height: 18, + borderRadius: '50%', + background: truthColor(score), + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: 9, + fontWeight: 700, + color: '#fff', + boxShadow: '0 1px 3px rgba(0,0,0,0.3)', + } + }, + `${score}` + ) + ); + } else { + children.push( + React.createElement( + "div", + { key: "truth-bar", className: "sw-truth-mini-bar" }, + React.createElement("div", { + className: "sw-truth-mini-marker", + style: { top: `${100 - score}%` }, + }) + ), + React.createElement( + "div", + { key: "truth-score", className: "sw-truth-mini-score", style: { background: truthColor(score) } }, + `${score}` + ) + ); + } root.render( React.createElement( @@ -396,30 +461,7 @@ function mountCard(container, spec, data, theme, post) { className: "sw-card-wrap", style: { width: spec?.size?.w, height: spec?.size?.h }, }, - React.createElement(CardRenderer, { - spec, - data, - themeTokens: tokens, - onAction: (action, value) => { - if (action?.type === "open_url" && value) { - try { window.open(value, "_blank"); } catch {} - } - }, - className: "", - }), - React.createElement( - "div", - { className: "sw-truth-mini-bar" }, - React.createElement("div", { - className: "sw-truth-mini-marker", - style: { top: `${100 - score}%` }, - }) - ), - React.createElement( - "div", - { className: "sw-truth-mini-score", style: { background: truthColor(score) } }, - `${score}` - ), + ...children ) ); @@ -2434,6 +2476,7 @@ export function createSimpleMarkerForPost(post, mapRef, markersRef, expandedElRe function renderSimple() { const activePost = root.__post || post; const isCamera = activePost?.content_type === 'camera'; + const isVideo = activePost?.content_type === 'video'; const isLive = activePost?.content_type === 'live'; const isWeather = activePost?.content_type === 'weather'; const liveColor = 'linear-gradient(135deg, rgba(220,38,38,0.95), rgba(239,68,68,0.95))'; @@ -2442,14 +2485,14 @@ export function createSimpleMarkerForPost(post, mapRef, markersRef, expandedElRe const weatherPin = '#3b82f6'; const color = isLive ? liveColor - : isCamera + : (isCamera || isVideo) ? 'linear-gradient(135deg, rgba(56, 189, 248, 0.9), rgba(59, 130, 246, 0.9))' : isWeather ? weatherColor : getMarkerColorForCategory(activePost?.category); const pinColor = isLive ? livePin - : isCamera + : (isCamera || isVideo) ? 'rgba(56, 189, 248, 0.9)' : isWeather ? weatherPin @@ -2489,41 +2532,81 @@ export function createSimpleMarkerForPost(post, mapRef, markersRef, expandedElRe // Weather city name for label const weatherCity = isWeather ? (activePost?.city || (activePost?.title || '').split(':')[0].replace(/^[^\w\s]+/, '').trim()) : ''; - // Camera thumbnail URL - const cameraThumbnail = isCamera ? (activePost?.thumbnail_url || activePost?.image_small || '') : ''; + // Camera/video thumbnail URL + const mediaThumbnail = (isCamera || isVideo) ? (activePost?.thumbnail_url || activePost?.image_small || '') : ''; - // Camera pin - square thumbnail with red live dot + // Camera pin - square thumbnail with red LED that pulses (bigger) + // Video pin - square thumbnail with play button overlay (bigger, no red LED) root.innerHTML = isCamera ? `
- ${cameraThumbnail ? `` : ``} + ${mediaThumbnail ? `` : ``}
+
+ ` : isVideo ? ` +
+
+ ${mediaThumbnail ? `` : ``} +
+ +
+
+
diff --git a/src/components/Map/postPriority.js b/src/components/Map/postPriority.js index 492e835..35b5b6b 100644 --- a/src/components/Map/postPriority.js +++ b/src/components/Map/postPriority.js @@ -138,8 +138,8 @@ export function prioritizePosts(posts, viewCenter, options = {}) { console.log('[prioritizePosts] post:', post.id, 'tier:', post.tier, 'content_type:', post.content_type); } - // Camera posts ALWAYS use simple markers (small blue pin with red dot) - if (contentType === 'camera') { + // Camera and video posts ALWAYS use simple markers (small blue pin with red dot) + if (contentType === 'camera' || contentType === 'video') { simpleMarkerPosts.push(post); continue; } @@ -166,18 +166,28 @@ export function prioritizePosts(posts, viewCenter, options = {}) { } // Fallback: old scoring system if no backend tiers - simpleMarkerPosts.length = 0; // Clear for fallback processing + // First, separate camera/video posts - they ALWAYS use simple markers + const forcedSimple = []; + const otherPosts = []; + for (const post of posts) { + const ct = (post?.content_type || '').toLowerCase(); + if (ct === 'camera' || ct === 'video') { + forcedSimple.push(post); + } else { + otherPosts.push(post); + } + } - const postsWithScores = posts.map(post => ({ + const postsWithScores = otherPosts.map(post => ({ post, - priority: calculatePostPriority(post, viewCenter, posts), + priority: calculatePostPriority(post, viewCenter, otherPosts), })); postsWithScores.sort((a, b) => b.priority - a.priority); - const clusters = clusterPosts(posts, clusterRadius); + const clusters = clusterPosts(otherPosts, clusterRadius); const finalFull = []; - const finalSimple = []; + const finalSimple = [...forcedSimple]; // Start with camera/video posts const clusterRepresentatives = new Set(); for (const [clusterId, clusterPostsList] of clusters.entries()) { diff --git a/src/components/Map/templateSpecs.js b/src/components/Map/templateSpecs.js index 30f0523..0ebd00f 100644 --- a/src/components/Map/templateSpecs.js +++ b/src/components/Map/templateSpecs.js @@ -11,13 +11,13 @@ export const TEMPLATE_SPECS = { polaroid: { mini_spec: { - size: { w: 180, h: 230 }, + size: { w: 150, h: 190 }, background: { type: "solid", value: "#ffffff" }, - radius: 8, + radius: 6, layers: [ - { id: "image", type: "image", x: 10, y: 10, w: 160, h: 160, bind: "data.image", radius: 4 }, - { id: "title", type: "text", x: 10, y: 176, w: 160, h: 28, bind: "data.headline", style: "text.polaroidTitle", maxLines: 2 }, - { id: "author", type: "text", x: 10, y: 206, w: 160, h: 18, bind: "data.source", style: "text.polaroidCaption", maxLines: 1 }, + { id: "image", type: "image", x: 8, y: 8, w: 134, h: 134, bind: "data.image", radius: 3 }, + { id: "title", type: "text", x: 8, y: 146, w: 134, h: 24, bind: "data.headline", style: "text.polaroidTitle", maxLines: 2 }, + { id: "author", type: "text", x: 8, y: 172, w: 134, h: 14, bind: "data.source", style: "text.polaroidCaption", maxLines: 1 }, ], }, full_spec: { diff --git a/src/components/Map/usePostsEngine.js b/src/components/Map/usePostsEngine.js index 982d8cc..9d669c4 100644 --- a/src/components/Map/usePostsEngine.js +++ b/src/components/Map/usePostsEngine.js @@ -717,7 +717,9 @@ export function usePostsEngine({ const have = new Set(next.map((x) => x.id)); for (const [id, entry] of wanted.entries()) { if (have.has(id)) continue; - if (entry.type === "simple") { + // Camera/video ALWAYS use simple markers regardless of priority + const ct = (entry.post?.content_type || '').toLowerCase(); + if (entry.type === "simple" || ct === 'camera' || ct === 'video') { createSimpleMarkerForPost(entry.post, mapRef, markersRef, expandedElRef, theme); } else { // Full cards use createMarkerForPost (template is decided by content_type) @@ -827,10 +829,12 @@ export function usePostsEngine({ const have = new Set(next.map((x) => x.id)); // add missing markers - use appropriate type based on priority + // Camera/video ALWAYS use simple markers regardless of priority for (const [id, { post, type }] of wanted.entries()) { if (have.has(id)) continue; - if (type === 'full') { + const ct = (post?.content_type || '').toLowerCase(); + if (type === 'full' && ct !== 'camera' && ct !== 'video') { createMarkerForPost(post, mapRef, markersRef, expandedElRef, theme); } else { createSimpleMarkerForPost(post, mapRef, markersRef, expandedElRef, theme); @@ -1416,9 +1420,9 @@ export function usePostsEngine({ const id = getId(normalized); const have = new Set((markersRef.current || []).map((x) => x.id)); if (id != null && !have.has(id)) { - // Camera posts always use simple markers (small blue pin with red dot) + // Camera and video posts always use simple markers (small blue pin with red dot) const ct = (normalized?.content_type || '').toLowerCase(); - if (ct === 'camera') { + if (ct === 'camera' || ct === 'video') { createSimpleMarkerForPost(normalized, mapRef, markersRef, expandedElRef, theme); } else { createMarkerForPost(normalized, mapRef, markersRef, expandedElRef, theme); diff --git a/src/theme/cardTokens.js b/src/theme/cardTokens.js index cc638f7..7bc0b89 100644 --- a/src/theme/cardTokens.js +++ b/src/theme/cardTokens.js @@ -23,8 +23,9 @@ export const cardTokens = { h1: { color: "#E9EEF8", fontSize: 22, fontWeight: 900, lineHeight: "26px" }, body: { color: "#D7DEED", fontSize: 14, fontWeight: 600, lineHeight: "18px" }, icon: { color: "rgba(255,255,255,0.3)", fontSize: 48, fontFamily: "Font Awesome 6 Free", display: "flex", alignItems: "center", justifyContent: "center", fontWeight: 900 }, - polaroidTitle: { color: "#1a1a1a", fontSize: 11, fontWeight: 700, lineHeight: "13px" }, - polaroidCaption: { color: "#666666", fontSize: 10, fontWeight: 500, lineHeight: "12px" }, + polaroidTitle: { color: "#1a1a1a", fontSize: 12, fontWeight: 800, lineHeight: "14px", letterSpacing: "-0.2px" }, + polaroidCaption: { color: "#888888", fontSize: 9, fontWeight: 600, lineHeight: "11px", fontStyle: "italic" }, + polaroidText: { color: "#333333", fontSize: 10, fontWeight: 500, lineHeight: "12px" }, }, chip: { news: { background: "rgba(78,161,255,0.18)", color: "#CFE6FF", fontWeight: 800, fontSize: 12, borderRadius: 999 }, @@ -59,8 +60,9 @@ export const cardTokens = { h1: { color: "#F0F7FF", fontSize: 22, fontWeight: 900, lineHeight: "26px" }, body: { color: "#D7E9FF", fontSize: 14, fontWeight: 700, lineHeight: "18px" }, icon: { color: "rgba(255,255,255,0.3)", fontSize: 48, fontFamily: "Font Awesome 6 Free", display: "flex", alignItems: "center", justifyContent: "center", fontWeight: 900 }, - polaroidTitle: { color: "#1a1a1a", fontSize: 11, fontWeight: 700, lineHeight: "13px" }, - polaroidCaption: { color: "#666666", fontSize: 10, fontWeight: 500, lineHeight: "12px" }, + polaroidTitle: { color: "#1a1a1a", fontSize: 12, fontWeight: 800, lineHeight: "14px", letterSpacing: "-0.2px" }, + polaroidCaption: { color: "#888888", fontSize: 9, fontWeight: 600, lineHeight: "11px", fontStyle: "italic" }, + polaroidText: { color: "#333333", fontSize: 10, fontWeight: 500, lineHeight: "12px" }, }, chip: { news: { background: "rgba(56,189,248,0.18)", color: "#D7F3FF", fontWeight: 900, fontSize: 12, borderRadius: 999 }, @@ -95,8 +97,9 @@ export const cardTokens = { h1: { color: "#0B1220", fontSize: 22, fontWeight: 900, lineHeight: "26px" }, body: { color: "#1A2740", fontSize: 14, fontWeight: 600, lineHeight: "18px" }, icon: { color: "rgba(0,0,0,0.15)", fontSize: 48, fontFamily: "Font Awesome 6 Free", display: "flex", alignItems: "center", justifyContent: "center", fontWeight: 900 }, - polaroidTitle: { color: "#1a1a1a", fontSize: 11, fontWeight: 700, lineHeight: "13px" }, - polaroidCaption: { color: "#666666", fontSize: 10, fontWeight: 500, lineHeight: "12px" }, + polaroidTitle: { color: "#1a1a1a", fontSize: 12, fontWeight: 800, lineHeight: "14px", letterSpacing: "-0.2px" }, + polaroidCaption: { color: "#888888", fontSize: 9, fontWeight: 600, lineHeight: "11px", fontStyle: "italic" }, + polaroidText: { color: "#333333", fontSize: 10, fontWeight: 500, lineHeight: "12px" }, }, chip: { news: { background: "rgba(30,107,255,0.12)", color: "#0B1220", fontWeight: 900, fontSize: 12, borderRadius: 999 },