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>
This commit is contained in:
SocioWire 2026-01-07 20:11:49 +00:00
parent 3e16629a32
commit 29e80a7f0e
24 changed files with 136 additions and 30 deletions

View File

@ -9,8 +9,9 @@
<link rel="sitemap" type="application/xml" title="Sitemap" href="/sitemap.xml" />
<!-- Favicon -->
<link rel="icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
<link rel="icon" type="image/png" href="/logo2026.png" />
<link rel="apple-touch-icon" href="/logo2026.png" />
<link rel="shortcut icon" type="image/png" href="/logo2026.png" />
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.json" />
@ -18,16 +19,17 @@
<!-- Meta pour partage (logo + titre + description) -->
<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: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:type" content="website" />
<meta name="facebook-domain-verification" content="o4iph92mh9fgqx58sn1b77ia08h8cp" />
<meta name="msvalidate.01" content="FF2D1A70EEA0A84AAF58B82A8E33F686" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<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: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:height" content="630" />
@ -46,6 +48,16 @@
}
}
</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">
{
"@context": "https://schema.org",
@ -70,8 +82,10 @@
</script>
<!-- Titre de la page -->
<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>
<body>
<h1 class="sr-only">SocioWire - Local News, Events, Deals and Social Posts on a Live Map</h1>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 946 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 170 KiB

BIN
public/logo2026.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -24,4 +24,4 @@
"background_color": "#050816",
"description": "SocioWire - Local news, events, market deals & friends on map",
"orientation": "portrait"
}
}

View File

@ -241,7 +241,7 @@ export default function App() {
<footer className="sw-footer">
<div className="sw-footer-inner">
<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">
<a href="/">Home</a>
<a href="/#map">Live Map</a>

View File

@ -107,7 +107,7 @@ async function resolveAssetRefsInPosts(posts) {
if (!Array.isArray(posts) || posts.length === 0) return posts;
const ids = [];
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 (Array.isArray(p?.media_urls)) {
@ -127,7 +127,7 @@ async function resolveAssetRefsInPosts(posts) {
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];
if (isAssetRef(v)) {
const id = v.replace("asset://", "");

View File

@ -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;
function getKeycloak() {
if (!kcInstance) {
@ -437,6 +446,9 @@ export function AuthProvider({ children }) {
setUser(w.username ? { username: w.username } : null);
setStatus("auth");
// Clear logout flag so session can restore on reload
try { sessionStorage.removeItem("sociowire:logout"); } catch {}
// update verified state from backend
await refreshProfile(t.access_token);
@ -447,7 +459,8 @@ export function AuthProvider({ children }) {
if (refreshTimer.current) clearInterval(refreshTimer.current);
refreshTimer.current = null;
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 {
sessionStorage.setItem("sociowire:logout", String(Date.now()));
} catch {}
@ -553,11 +566,13 @@ export function AuthProvider({ children }) {
}
setUser(w.username ? { username: w.username } : null);
try { sessionStorage.removeItem("sociowire:logout"); } catch {}
setStatus("auth");
await refreshProfile(t.access_token);
} catch (err) {
console.error("[Auth] Post-SSO verification failed:", err);
// Still set as authenticated since we have tokens
try { sessionStorage.removeItem("sociowire:logout"); } catch {}
setStatus("auth");
}
@ -579,6 +594,12 @@ export function AuthProvider({ children }) {
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;
try {
if (sessionStorage.getItem("sociowire:logout")) {

View File

@ -406,7 +406,7 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
<header className="topbar">
<div className="brand-stack">
<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="main-title">SOCIOWIRE.com</div>
<div className="sub-title">Wired to life</div>

View File

@ -4,6 +4,7 @@ import { useAuth } from "../../auth/AuthContext";
import "./Live.css";
const API_BASE = (import.meta?.env?.VITE_API_BASE_URL || "").replace(/\/+$/, "");
const LIVEKIT_API = "https://live.us1.sociowire.com";
/**
* LiveKit-based broadcast component
@ -18,6 +19,19 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
const [localStream, setLocalStream] = useState(null);
const [showConfirm, setShowConfirm] = useState(false);
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);
@ -90,7 +104,7 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
onStreamCreated({
roomName,
title: title.trim(),
coords,
coords: liveCoords,
});
}
@ -106,8 +120,8 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
snippet: "Live now",
category: "EVENT",
sub_category: "Live",
lat: coords?.lat,
lon: coords?.lon,
lat: liveCoords?.lat,
lon: liveCoords?.lon,
author: username,
source: "sociowire",
visibility: "public",
@ -128,13 +142,31 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
console.error("[LiveKit] Failed to create live post:", err);
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) => {
await stopBroadcast();
if (localStream) {
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) {
try {
const res = await fetch(`${API_BASE}/api/live/finalize`, {
@ -146,7 +178,7 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
body: JSON.stringify({
post_id: livePostId,
save: !!save,
video_url: "",
video_key: videoKey,
}),
});
if (res.ok && save) {
@ -159,7 +191,7 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
if (!res.ok) {
const errText = await res.text();
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) {
console.error("[LiveKit] Failed to finalize live post:", err);
@ -167,7 +199,7 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
}
}
onClose?.();
}, [stopBroadcast, localStream, onClose, livePostId, authToken, username]);
}, [stopBroadcast, localStream, onClose, livePostId, authToken, roomName]);
const handleBackdropClick = (e) => {
if (e.target === e.currentTarget && !isLive) {

View File

@ -148,6 +148,18 @@ export function useLiveKit({ roomName, userID, username, token: authToken }) {
roomRef.current = room;
setIsLive(true);
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) {
console.error("[LiveKit] Failed to start broadcast:", err);
setError(err.message || "Failed to start broadcast");

View File

@ -235,9 +235,10 @@ function applyPostDetails(modalWrap, post) {
const summaryEl = modalWrap.querySelector(".sw-modal-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';
if (!isCamera) {
const isVideo = post?.content_type === 'video';
if (!isCamera && !isVideo) {
const heroGallery = collectGalleryImages(post);
const heroImageSrc = heroGallery[0] || heroImageForPost(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)
});
// 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 isVideo = postData.content_type === 'video';
if (isCamera) {
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 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>
</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 ? `
<div class="sw-camera-hero" style="position:relative;width:100%;padding-top:56.25%;background:#000;border-radius:12px;overflow:hidden;">
<video
@ -1210,6 +1215,21 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
LIVE
</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 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>`

View File

@ -295,7 +295,13 @@ export default function PostCard({ post, selected, onSelect }) {
<article
className={"post-card sw-wall-card" + (selected ? " selected" : "")}
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);
}}
>
@ -397,7 +403,7 @@ export default function PostCard({ post, selected, onSelect }) {
requestAuth("Please sign in or create a free account to like this post.");
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);
if (res?.ok) {
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.");
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);
}}
>
@ -434,7 +440,7 @@ export default function PostCard({ post, selected, onSelect }) {
onClick={async (e) => {
e.stopPropagation();
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 {
if (navigator.share) {
await navigator.share({ title: title || "SocioWire", url: shareUrl });

View File

@ -209,6 +209,7 @@
gap:.4rem;
}
/* Crosshair pour création */
.map-crosshair{
left:50%;

View File

@ -13,11 +13,11 @@
}
.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;
width:52px; height:52px; flex-shrink:0;
margin: -6px 0;
}
.logo-circle img{
width:100%; height:100%; object-fit:contain;
}
.title-block{ display:flex; flex-direction:column; line-height:1.05; min-width:0; }