Update branding and fix video content display
- Update favicon and icons with new 2026 branding - Fix video content display - skip hero replacement for video posts - Add video_url to asset resolution in client.js - Update LiveKit broadcast with egress recording - Fix auth context and topbar styling - Update PostCard and map styling 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
22
index.html
|
|
@ -9,8 +9,9 @@
|
||||||
<link rel="sitemap" type="application/xml" title="Sitemap" href="/sitemap.xml" />
|
<link rel="sitemap" type="application/xml" title="Sitemap" href="/sitemap.xml" />
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" type="image/png" href="/logo2026.png" />
|
||||||
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="/logo2026.png" />
|
||||||
|
<link rel="shortcut icon" type="image/png" href="/logo2026.png" />
|
||||||
|
|
||||||
<!-- PWA Manifest -->
|
<!-- PWA Manifest -->
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
|
@ -18,16 +19,17 @@
|
||||||
<!-- Meta pour partage (logo + titre + description) -->
|
<!-- Meta pour partage (logo + titre + description) -->
|
||||||
<meta property="og:title" content="SocioWire.com - Wired to life" />
|
<meta property="og:title" content="SocioWire.com - Wired to life" />
|
||||||
<meta property="og:description" content="Local news, events, market deals & friends on the map." />
|
<meta property="og:description" content="Local news, events, market deals & friends on the map." />
|
||||||
<meta property="og:image" content="https://sociowire.com/icons/og-image.png" />
|
<meta property="og:image" content="https://sociowire.com/logo2026.png" />
|
||||||
<meta property="og:url" content="https://sociowire.com" />
|
<meta property="og:url" content="https://sociowire.com" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta name="facebook-domain-verification" content="o4iph92mh9fgqx58sn1b77ia08h8cp" />
|
<meta name="facebook-domain-verification" content="o4iph92mh9fgqx58sn1b77ia08h8cp" />
|
||||||
|
<meta name="msvalidate.01" content="FF2D1A70EEA0A84AAF58B82A8E33F686" />
|
||||||
|
|
||||||
<!-- Twitter Card -->
|
<!-- Twitter Card -->
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:title" content="SocioWire.com - Wired to life" />
|
<meta name="twitter:title" content="SocioWire.com - Wired to life" />
|
||||||
<meta name="twitter:description" content="Local news, events, market deals & friends on the map." />
|
<meta name="twitter:description" content="Local news, events, market deals & friends on the map." />
|
||||||
<meta name="twitter:image" content="https://www.sociowire.com/icons/og-image.png" />
|
<meta name="twitter:image" content="https://sociowire.com/logo2026.png" />
|
||||||
<meta property="og:image:width" content="1200" />
|
<meta property="og:image:width" content="1200" />
|
||||||
<meta property="og:image:height" content="630" />
|
<meta property="og:image:height" content="630" />
|
||||||
|
|
||||||
|
|
@ -46,6 +48,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "SocioWire",
|
||||||
|
"url": "https://sociowire.com",
|
||||||
|
"logo": "https://sociowire.com/logo2026.png",
|
||||||
|
"sameAs": []
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
{
|
{
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
|
|
@ -70,8 +82,10 @@
|
||||||
</script>
|
</script>
|
||||||
<!-- Titre de la page -->
|
<!-- Titre de la page -->
|
||||||
<title>SocioWire.com - Wired to life</title>
|
<title>SocioWire.com - Wired to life</title>
|
||||||
|
<style>.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<h1 class="sr-only">SocioWire - Local News, Events, Deals and Social Posts on a Live Map</h1>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 946 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 243 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
|
@ -241,7 +241,7 @@ export default function App() {
|
||||||
<footer className="sw-footer">
|
<footer className="sw-footer">
|
||||||
<div className="sw-footer-inner">
|
<div className="sw-footer-inner">
|
||||||
<div className="sw-footer-brand">SOCIOWIRE</div>
|
<div className="sw-footer-brand">SOCIOWIRE</div>
|
||||||
<div className="sw-footer-brand">© Sociowire v0.0001</div>
|
<div className="sw-footer-brand">© Sociowire v0.0002</div>
|
||||||
<nav className="sw-footer-nav" aria-label="Site">
|
<nav className="sw-footer-nav" aria-label="Site">
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
<a href="/#map">Live Map</a>
|
<a href="/#map">Live Map</a>
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ async function resolveAssetRefsInPosts(posts) {
|
||||||
if (!Array.isArray(posts) || posts.length === 0) return posts;
|
if (!Array.isArray(posts) || posts.length === 0) return posts;
|
||||||
const ids = [];
|
const ids = [];
|
||||||
for (const p of posts) {
|
for (const p of posts) {
|
||||||
[p?.image_small, p?.image_medium, p?.image_large, p?.image, p?.media_url].forEach((v) => {
|
[p?.image_small, p?.image_medium, p?.image_large, p?.image, p?.media_url, p?.video_url].forEach((v) => {
|
||||||
if (isAssetRef(v)) ids.push(v.replace("asset://", ""));
|
if (isAssetRef(v)) ids.push(v.replace("asset://", ""));
|
||||||
});
|
});
|
||||||
if (Array.isArray(p?.media_urls)) {
|
if (Array.isArray(p?.media_urls)) {
|
||||||
|
|
@ -127,7 +127,7 @@ async function resolveAssetRefsInPosts(posts) {
|
||||||
clone.id = clone.post_id;
|
clone.id = clone.post_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
["image_small", "image_medium", "image_large", "image", "media_url"].forEach((k) => {
|
["image_small", "image_medium", "image_large", "image", "media_url", "video_url"].forEach((k) => {
|
||||||
const v = clone[k];
|
const v = clone[k];
|
||||||
if (isAssetRef(v)) {
|
if (isAssetRef(v)) {
|
||||||
const id = v.replace("asset://", "");
|
const id = v.replace("asset://", "");
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,15 @@ function isCrossOriginSSO() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if we're on the isolated apps subdomain (no SSO sync with main site)
|
||||||
|
function isAppsSubdomain() {
|
||||||
|
try {
|
||||||
|
return window.location.hostname === "apps.sociowire.com";
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let kcInstance = null;
|
let kcInstance = null;
|
||||||
function getKeycloak() {
|
function getKeycloak() {
|
||||||
if (!kcInstance) {
|
if (!kcInstance) {
|
||||||
|
|
@ -437,6 +446,9 @@ export function AuthProvider({ children }) {
|
||||||
setUser(w.username ? { username: w.username } : null);
|
setUser(w.username ? { username: w.username } : null);
|
||||||
setStatus("auth");
|
setStatus("auth");
|
||||||
|
|
||||||
|
// Clear logout flag so session can restore on reload
|
||||||
|
try { sessionStorage.removeItem("sociowire:logout"); } catch {}
|
||||||
|
|
||||||
// ✅ update verified state from backend
|
// ✅ update verified state from backend
|
||||||
await refreshProfile(t.access_token);
|
await refreshProfile(t.access_token);
|
||||||
|
|
||||||
|
|
@ -447,7 +459,8 @@ export function AuthProvider({ children }) {
|
||||||
if (refreshTimer.current) clearInterval(refreshTimer.current);
|
if (refreshTimer.current) clearInterval(refreshTimer.current);
|
||||||
refreshTimer.current = null;
|
refreshTimer.current = null;
|
||||||
const redirectUri = window.location.origin + window.location.pathname;
|
const redirectUri = window.location.origin + window.location.pathname;
|
||||||
const doSsoLogout = keycloakEnabled();
|
// Don't do SSO logout for apps subdomain - keep it isolated
|
||||||
|
const doSsoLogout = keycloakEnabled() && !isAppsSubdomain();
|
||||||
try {
|
try {
|
||||||
sessionStorage.setItem("sociowire:logout", String(Date.now()));
|
sessionStorage.setItem("sociowire:logout", String(Date.now()));
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
@ -553,11 +566,13 @@ export function AuthProvider({ children }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
setUser(w.username ? { username: w.username } : null);
|
setUser(w.username ? { username: w.username } : null);
|
||||||
|
try { sessionStorage.removeItem("sociowire:logout"); } catch {}
|
||||||
setStatus("auth");
|
setStatus("auth");
|
||||||
await refreshProfile(t.access_token);
|
await refreshProfile(t.access_token);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Auth] Post-SSO verification failed:", err);
|
console.error("[Auth] Post-SSO verification failed:", err);
|
||||||
// Still set as authenticated since we have tokens
|
// Still set as authenticated since we have tokens
|
||||||
|
try { sessionStorage.removeItem("sociowire:logout"); } catch {}
|
||||||
setStatus("auth");
|
setStatus("auth");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -579,6 +594,12 @@ export function AuthProvider({ children }) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip silent SSO for apps subdomain - keep it isolated from main site
|
||||||
|
if (isAppsSubdomain()) {
|
||||||
|
console.log("[Auth] Skipping silent SSO check (apps subdomain - isolated)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
let kc = null;
|
let kc = null;
|
||||||
try {
|
try {
|
||||||
if (sessionStorage.getItem("sociowire:logout")) {
|
if (sessionStorage.getItem("sociowire:logout")) {
|
||||||
|
|
|
||||||
|
|
@ -406,7 +406,7 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
|
||||||
<header className="topbar">
|
<header className="topbar">
|
||||||
<div className="brand-stack">
|
<div className="brand-stack">
|
||||||
<div className="brand-toprow">
|
<div className="brand-toprow">
|
||||||
<div className="logo-circle">SW</div>
|
<div className="logo-circle"><img src="/logo2026.png" alt="SocioWire" /></div>
|
||||||
<div className="title-block">
|
<div className="title-block">
|
||||||
<div className="main-title">SOCIOWIRE.com</div>
|
<div className="main-title">SOCIOWIRE.com</div>
|
||||||
<div className="sub-title">Wired to life</div>
|
<div className="sub-title">Wired to life</div>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useAuth } from "../../auth/AuthContext";
|
||||||
import "./Live.css";
|
import "./Live.css";
|
||||||
|
|
||||||
const API_BASE = (import.meta?.env?.VITE_API_BASE_URL || "").replace(/\/+$/, "");
|
const API_BASE = (import.meta?.env?.VITE_API_BASE_URL || "").replace(/\/+$/, "");
|
||||||
|
const LIVEKIT_API = "https://live.us1.sociowire.com";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LiveKit-based broadcast component
|
* LiveKit-based broadcast component
|
||||||
|
|
@ -18,6 +19,19 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
||||||
const [localStream, setLocalStream] = useState(null);
|
const [localStream, setLocalStream] = useState(null);
|
||||||
const [showConfirm, setShowConfirm] = useState(false);
|
const [showConfirm, setShowConfirm] = useState(false);
|
||||||
const [livePostId, setLivePostId] = useState(null);
|
const [livePostId, setLivePostId] = useState(null);
|
||||||
|
const [liveCoords] = useState(() => {
|
||||||
|
if (coords && typeof coords === "object") {
|
||||||
|
if (Array.isArray(coords) && coords.length >= 2) {
|
||||||
|
return { lon: coords[0], lat: coords[1] };
|
||||||
|
}
|
||||||
|
const lat = coords.lat ?? coords.latitude;
|
||||||
|
const lon = coords.lon ?? coords.lng ?? coords.longitude;
|
||||||
|
if (Number.isFinite(lat) && Number.isFinite(lon)) {
|
||||||
|
return { lat, lon };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { lat: null, lon: null };
|
||||||
|
});
|
||||||
|
|
||||||
const videoRef = useRef(null);
|
const videoRef = useRef(null);
|
||||||
|
|
||||||
|
|
@ -90,7 +104,7 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
||||||
onStreamCreated({
|
onStreamCreated({
|
||||||
roomName,
|
roomName,
|
||||||
title: title.trim(),
|
title: title.trim(),
|
||||||
coords,
|
coords: liveCoords,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,8 +120,8 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
||||||
snippet: "Live now",
|
snippet: "Live now",
|
||||||
category: "EVENT",
|
category: "EVENT",
|
||||||
sub_category: "Live",
|
sub_category: "Live",
|
||||||
lat: coords?.lat,
|
lat: liveCoords?.lat,
|
||||||
lon: coords?.lon,
|
lon: liveCoords?.lon,
|
||||||
author: username,
|
author: username,
|
||||||
source: "sociowire",
|
source: "sociowire",
|
||||||
visibility: "public",
|
visibility: "public",
|
||||||
|
|
@ -128,13 +142,31 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
||||||
console.error("[LiveKit] Failed to create live post:", err);
|
console.error("[LiveKit] Failed to create live post:", err);
|
||||||
alert("Live started, but we couldn't create the live post.");
|
alert("Live started, but we couldn't create the live post.");
|
||||||
}
|
}
|
||||||
}, [title, localStream, startBroadcast, roomName, coords, onStreamCreated, authToken, username]);
|
}, [title, localStream, startBroadcast, roomName, liveCoords, onStreamCreated, authToken, username]);
|
||||||
|
|
||||||
const handleEndStream = useCallback(async (save) => {
|
const handleEndStream = useCallback(async (save) => {
|
||||||
await stopBroadcast();
|
await stopBroadcast();
|
||||||
if (localStream) {
|
if (localStream) {
|
||||||
localStream.getTracks().forEach((t) => t.stop());
|
localStream.getTracks().forEach((t) => t.stop());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop egress recording and get video key
|
||||||
|
let videoKey = "";
|
||||||
|
try {
|
||||||
|
const egressRes = await fetch(`${LIVEKIT_API}/api/livekit/egress/stop`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ room: roomName, save: !!save }),
|
||||||
|
});
|
||||||
|
if (egressRes.ok) {
|
||||||
|
const egressData = await egressRes.json();
|
||||||
|
videoKey = egressData.key || "";
|
||||||
|
console.log("[LiveKit] Egress stopped, video key:", videoKey);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[LiveKit] Failed to stop egress:", err);
|
||||||
|
}
|
||||||
|
|
||||||
if (livePostId) {
|
if (livePostId) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/api/live/finalize`, {
|
const res = await fetch(`${API_BASE}/api/live/finalize`, {
|
||||||
|
|
@ -146,7 +178,7 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
post_id: livePostId,
|
post_id: livePostId,
|
||||||
save: !!save,
|
save: !!save,
|
||||||
video_url: "",
|
video_key: videoKey,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (res.ok && save) {
|
if (res.ok && save) {
|
||||||
|
|
@ -159,7 +191,7 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errText = await res.text();
|
const errText = await res.text();
|
||||||
console.error("[LiveKit] Finalize failed:", res.status, errText);
|
console.error("[LiveKit] Finalize failed:", res.status, errText);
|
||||||
alert("We couldn't save the live post. Please verify your email and try again.");
|
alert("We couldn't save the live post. Please try again.");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[LiveKit] Failed to finalize live post:", err);
|
console.error("[LiveKit] Failed to finalize live post:", err);
|
||||||
|
|
@ -167,7 +199,7 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onClose?.();
|
onClose?.();
|
||||||
}, [stopBroadcast, localStream, onClose, livePostId, authToken, username]);
|
}, [stopBroadcast, localStream, onClose, livePostId, authToken, roomName]);
|
||||||
|
|
||||||
const handleBackdropClick = (e) => {
|
const handleBackdropClick = (e) => {
|
||||||
if (e.target === e.currentTarget && !isLive) {
|
if (e.target === e.currentTarget && !isLive) {
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,18 @@ export function useLiveKit({ roomName, userID, username, token: authToken }) {
|
||||||
roomRef.current = room;
|
roomRef.current = room;
|
||||||
setIsLive(true);
|
setIsLive(true);
|
||||||
console.log("[LiveKit] Broadcasting started");
|
console.log("[LiveKit] Broadcasting started");
|
||||||
|
|
||||||
|
// Start egress recording
|
||||||
|
try {
|
||||||
|
await fetch(`${LIVEKIT_API}/api/livekit/egress/start`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ room: roomName }),
|
||||||
|
});
|
||||||
|
console.log("[LiveKit] Egress recording started");
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[LiveKit] Failed to start egress:", err);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[LiveKit] Failed to start broadcast:", err);
|
console.error("[LiveKit] Failed to start broadcast:", err);
|
||||||
setError(err.message || "Failed to start broadcast");
|
setError(err.message || "Failed to start broadcast");
|
||||||
|
|
|
||||||
|
|
@ -235,9 +235,10 @@ function applyPostDetails(modalWrap, post) {
|
||||||
const summaryEl = modalWrap.querySelector(".sw-modal-summary");
|
const summaryEl = modalWrap.querySelector(".sw-modal-summary");
|
||||||
if (summaryEl) summaryEl.textContent = summary;
|
if (summaryEl) summaryEl.textContent = summary;
|
||||||
|
|
||||||
// Skip hero replacement for camera posts - they have video content
|
// Skip hero replacement for camera and video posts - they have video content
|
||||||
const isCamera = post?.content_type === 'camera';
|
const isCamera = post?.content_type === 'camera';
|
||||||
if (!isCamera) {
|
const isVideo = post?.content_type === 'video';
|
||||||
|
if (!isCamera && !isVideo) {
|
||||||
const heroGallery = collectGalleryImages(post);
|
const heroGallery = collectGalleryImages(post);
|
||||||
const heroImageSrc = heroGallery[0] || heroImageForPost(post);
|
const heroImageSrc = heroGallery[0] || heroImageForPost(post);
|
||||||
const heroFullSrc = heroGallery[0] || fullImageForPost(post);
|
const heroFullSrc = heroGallery[0] || fullImageForPost(post);
|
||||||
|
|
@ -1015,11 +1016,15 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
|
||||||
title: postData.title?.substring(0, 30)
|
title: postData.title?.substring(0, 30)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Camera posts now use the regular modal with video content
|
// Camera and video posts use the regular modal with video content
|
||||||
const isCamera = postData.content_type === 'camera';
|
const isCamera = postData.content_type === 'camera';
|
||||||
|
const isVideo = postData.content_type === 'video';
|
||||||
if (isCamera) {
|
if (isCamera) {
|
||||||
console.log('[openCenteredOverlay] Opening camera as regular post with video');
|
console.log('[openCenteredOverlay] Opening camera as regular post with video');
|
||||||
}
|
}
|
||||||
|
if (isVideo) {
|
||||||
|
console.log('[openCenteredOverlay] Opening saved video post:', postData.video_url);
|
||||||
|
}
|
||||||
|
|
||||||
const data = adaptPostToTemplateData(postData, { size: "full" });
|
const data = adaptPostToTemplateData(postData, { size: "full" });
|
||||||
const cat = normCatLabel(postData);
|
const cat = normCatLabel(postData);
|
||||||
|
|
@ -1191,7 +1196,7 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
|
||||||
<button class="sw-truth-rail-btn sw-truth-rail-btn-down" type="button">FALSE</button>
|
<button class="sw-truth-rail-btn sw-truth-rail-btn-down" type="button">FALSE</button>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
<div class="sw-modal-hero" ${isCamera ? 'style="flex:1;max-width:100%;"' : ''}>
|
<div class="sw-modal-hero" ${(isCamera || isVideo) ? 'style="flex:1;max-width:100%;"' : ''}>
|
||||||
${isCamera ? `
|
${isCamera ? `
|
||||||
<div class="sw-camera-hero" style="position:relative;width:100%;padding-top:56.25%;background:#000;border-radius:12px;overflow:hidden;">
|
<div class="sw-camera-hero" style="position:relative;width:100%;padding-top:56.25%;background:#000;border-radius:12px;overflow:hidden;">
|
||||||
<video
|
<video
|
||||||
|
|
@ -1210,6 +1215,21 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
|
||||||
LIVE
|
LIVE
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
` : isVideo ? `
|
||||||
|
<div class="sw-video-hero" style="position:relative;width:100%;padding-top:56.25%;background:#000;border-radius:12px;overflow:hidden;">
|
||||||
|
<video
|
||||||
|
controls
|
||||||
|
playsinline
|
||||||
|
style="position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:#000;"
|
||||||
|
src="${escapeHtml(postData.video_url || '')}"
|
||||||
|
>
|
||||||
|
<source src="${escapeHtml(postData.video_url || '')}" type="video/mp4">
|
||||||
|
</video>
|
||||||
|
<div class="sw-video-badge" style="position:absolute;top:12px;left:12px;display:flex;align-items:center;gap:6px;padding:4px 10px;background:rgba(59,130,246,0.9);border-radius:4px;font-size:11px;font-weight:700;color:#fff;text-transform:uppercase;">
|
||||||
|
<i class="fa-solid fa-video"></i>
|
||||||
|
RECORDED
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
` : img
|
` : img
|
||||||
? `<img src="${escapeHtml(img)}" data-fullsrc="${escapeHtml(fullImageForPost(postData))}" alt="" loading="lazy" />`
|
? `<img src="${escapeHtml(img)}" data-fullsrc="${escapeHtml(fullImageForPost(postData))}" alt="" loading="lazy" />`
|
||||||
: `<div class="sw-modal-hero-fallback"><div class="sw-modal-hero-fallback-inner"><i class="${categoryIconClass(postData)}"></i><span>${escapeHtml(normCatLabel(postData))}</span></div></div>`
|
: `<div class="sw-modal-hero-fallback"><div class="sw-modal-hero-fallback-inner"><i class="${categoryIconClass(postData)}"></i><span>${escapeHtml(normCatLabel(postData))}</span></div></div>`
|
||||||
|
|
|
||||||
|
|
@ -295,7 +295,13 @@ export default function PostCard({ post, selected, onSelect }) {
|
||||||
<article
|
<article
|
||||||
className={"post-card sw-wall-card" + (selected ? " selected" : "")}
|
className={"post-card sw-wall-card" + (selected ? " selected" : "")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
trackEvent("post_open", { post_id: postId, context: "wall" });
|
trackEvent("post_view", {
|
||||||
|
post_id: postId,
|
||||||
|
post_title: title?.slice(0, 100),
|
||||||
|
post_category: cat,
|
||||||
|
post_author: author,
|
||||||
|
content_type: "post",
|
||||||
|
});
|
||||||
onSelect && onSelect(post);
|
onSelect && onSelect(post);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -397,7 +403,7 @@ export default function PostCard({ post, selected, onSelect }) {
|
||||||
requestAuth("Please sign in or create a free account to like this post.");
|
requestAuth("Please sign in or create a free account to like this post.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
trackEvent("post_like_click", { post_id: postId, context: "wall" });
|
trackEvent("post_like", { post_id: postId, post_title: title?.slice(0, 100), post_category: cat });
|
||||||
const res = await togglePostLike(postId, !liked);
|
const res = await togglePostLike(postId, !liked);
|
||||||
if (res?.ok) {
|
if (res?.ok) {
|
||||||
setLiked(!!res.liked);
|
setLiked(!!res.liked);
|
||||||
|
|
@ -422,7 +428,7 @@ export default function PostCard({ post, selected, onSelect }) {
|
||||||
requestAuth("Please sign in or create a free account to comment.");
|
requestAuth("Please sign in or create a free account to comment.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
trackEvent("post_comment_open", { post_id: postId, context: "wall" });
|
trackEvent("post_comment", { post_id: postId, post_title: title?.slice(0, 100), post_category: cat });
|
||||||
if (onSelect) onSelect(post);
|
if (onSelect) onSelect(post);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -434,7 +440,7 @@ export default function PostCard({ post, selected, onSelect }) {
|
||||||
onClick={async (e) => {
|
onClick={async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const shareUrl = `${window.location.origin}/p/${postId}`;
|
const shareUrl = `${window.location.origin}/p/${postId}`;
|
||||||
trackEvent("post_share_click", { post_id: postId, context: "wall" });
|
trackEvent("post_share", { post_id: postId, post_title: title?.slice(0, 100), post_category: cat });
|
||||||
try {
|
try {
|
||||||
if (navigator.share) {
|
if (navigator.share) {
|
||||||
await navigator.share({ title: title || "SocioWire", url: shareUrl });
|
await navigator.share({ title: title || "SocioWire", url: shareUrl });
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,7 @@
|
||||||
gap:.4rem;
|
gap:.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Crosshair pour création */
|
/* Crosshair pour création */
|
||||||
.map-crosshair{
|
.map-crosshair{
|
||||||
left:50%;
|
left:50%;
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-circle{
|
.logo-circle{
|
||||||
width:40px; height:40px; border-radius:50%; flex-shrink:0;
|
width:52px; height:52px; flex-shrink:0;
|
||||||
background-image:url("/icons/logo-master.png");
|
margin: -6px 0;
|
||||||
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);
|
.logo-circle img{
|
||||||
color:transparent;
|
width:100%; height:100%; object-fit:contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-block{ display:flex; flex-direction:column; line-height:1.05; min-width:0; }
|
.title-block{ display:flex; flex-direction:column; line-height:1.05; min-width:0; }
|
||||||
|
|
|
||||||