Fix email verification redirect handling

Add 'validated' to the list of accepted URL params for email verification,
since Keycloak sends validated=1 instead of verified=1.

🤖 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-10 21:29:30 +00:00
parent e8c2a4cda1
commit 177f0034d6
4 changed files with 172 additions and 35 deletions

View File

@ -347,6 +347,7 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
const verified = const verified =
get("verified") || get("verified") ||
get("validated") ||
get("email_verified") || get("email_verified") ||
get("emailVerified") || get("emailVerified") ||
get("kc_verified"); get("kc_verified");
@ -393,8 +394,8 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
} }
const touched = const touched =
qs.has("verified") || qs.has("email_verified") || qs.has("emailVerified") || qs.has("signup") || qs.has("notice") || qs.has("verified") || qs.has("validated") || qs.has("email_verified") || qs.has("emailVerified") || qs.has("signup") || qs.has("notice") ||
hs.has("verified") || hs.has("email_verified") || hs.has("emailVerified") || hs.has("signup") || hs.has("notice"); hs.has("verified") || hs.has("validated") || hs.has("email_verified") || hs.has("emailVerified") || hs.has("signup") || hs.has("notice");
if (touched) { if (touched) {
const cleanHash = hash.includes("?") ? hash.split("?")[0] : hash; const cleanHash = hash.includes("?") ? hash.split("?")[0] : hash;

View File

@ -5,7 +5,7 @@ import "../../styles/mapMarkers.css";
import "../../styles/posts.css"; import "../../styles/posts.css";
import "../../styles/filters.css"; import "../../styles/filters.css";
import { createPost, fetchPostPreview, resolveAssetRef } from "../../api/client"; import { createPost, fetchPostPreview, resolveAssetRef, fetchWeatherPosts } from "../../api/client";
import { LiveKitBroadcast, LiveKitViewerModal } from "../Live"; import { LiveKitBroadcast, LiveKitViewerModal } from "../Live";
import { WeatherModal } from "../Weather"; import { WeatherModal } from "../Weather";
import { import {
@ -144,10 +144,10 @@ export default function MapView({
}); });
const [subFilter, setSubFilter] = useState("All"); const [subFilter, setSubFilter] = useState("All");
const [mainNewsOnly, setMainNewsOnly] = useState(false); const [mainNewsOnly, setMainNewsOnly] = useState(false);
const DAY_NIGHT_KEY = "sociowire:dayNight"; const WEATHER_VIEW_KEY = "sociowire:weatherView";
const [dayNightEnabled, setDayNightEnabled] = useState(() => { const [weatherViewEnabled, setWeatherViewEnabled] = useState(() => {
try { try {
return localStorage.getItem(DAY_NIGHT_KEY) === "true"; return localStorage.getItem(WEATHER_VIEW_KEY) === "true";
} catch { } catch {
return false; return false;
} }
@ -170,9 +170,9 @@ export default function MapView({
useEffect(() => { useEffect(() => {
try { try {
localStorage.setItem(DAY_NIGHT_KEY, dayNightEnabled ? "true" : "false"); localStorage.setItem(WEATHER_VIEW_KEY, weatherViewEnabled ? "true" : "false");
} catch {} } catch {}
}, [dayNightEnabled]); }, [weatherViewEnabled]);
useEffect(() => { useEffect(() => {
if (clusterFocus) setClusterFocus(null); if (clusterFocus) setClusterFocus(null);
@ -198,6 +198,9 @@ export default function MapView({
const [liveCoords, setLiveCoords] = useState(null); const [liveCoords, setLiveCoords] = useState(null);
// Weather modal state // Weather modal state
const [activeWeatherPost, setActiveWeatherPost] = useState(null); const [activeWeatherPost, setActiveWeatherPost] = useState(null);
// Weather cities widget (top left under search)
const [weatherCities, setWeatherCities] = useState([]);
const [nearestWeather, setNearestWeather] = useState(null);
const openedPostRef = useRef(null); const openedPostRef = useRef(null);
const queryPostRef = useRef(false); const queryPostRef = useRef(false);
const overlayTimerRef = useRef(null); const overlayTimerRef = useRef(null);
@ -447,6 +450,7 @@ export default function MapView({
onSelectPost, onSelectPost,
username, username,
debugEnabled, debugEnabled,
weatherEnabled: weatherViewEnabled,
}); });
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
@ -1206,6 +1210,82 @@ export default function MapView({
return () => map.off("movestart", handleMove); return () => map.off("movestart", handleMove);
}, [mapRef, searchQuery]); }, [mapRef, searchQuery]);
// Fetch weather cities on mount
useEffect(() => {
const loadWeatherCities = async () => {
try {
const posts = await fetchWeatherPosts();
if (Array.isArray(posts) && posts.length > 0) {
// Parse weather data from posts
const cities = posts.map(p => {
const title = p.title || '';
const tempMatch = title.match(/:\s*(-?\d+)°/);
const cityMatch = title.match(/^[^\w]*([A-Za-zÀ-ÿ\s]+):/);
const temp = tempMatch ? parseInt(tempMatch[1], 10) : null;
const city = cityMatch ? cityMatch[1].trim() : '';
// Detect weather icon from emoji in title
let icon = '🌡️';
if (title.includes('☀️')) icon = '☀️';
else if (title.includes('🌙')) icon = '🌙';
else if (title.includes('⛅')) icon = '⛅';
else if (title.includes('☁️')) icon = '☁️';
else if (title.includes('🌫️')) icon = '🌫️';
else if (title.includes('🌧️')) icon = '🌧️';
else if (title.includes('❄️')) icon = '❄️';
else if (title.includes('🌦️')) icon = '🌦️';
else if (title.includes('🌨️')) icon = '🌨️';
else if (title.includes('⛈️')) icon = '⛈️';
return { ...p, city, temp, icon };
}).filter(c => c.city && c.temp !== null);
setWeatherCities(cities);
}
} catch (err) {
console.warn('[Weather] Failed to load cities:', err);
}
};
loadWeatherCities();
// Refresh every 10 minutes
const interval = setInterval(loadWeatherCities, 10 * 60 * 1000);
return () => clearInterval(interval);
}, []);
// Find nearest weather city when map moves
useEffect(() => {
const map = mapRef.current;
if (!map || weatherCities.length === 0) return;
const updateNearest = () => {
try {
const center = map.getCenter();
if (!center) return;
let nearest = null;
let minDist = Infinity;
for (const w of weatherCities) {
if (w.lat == null || w.lon == null) continue;
const dist = Math.sqrt(
Math.pow(center.lat - w.lat, 2) + Math.pow(center.lng - w.lon, 2)
);
if (dist < minDist) {
minDist = dist;
nearest = w;
}
}
if (nearest) setNearestWeather(nearest);
} catch {}
};
updateNearest();
map.on("idle", updateNearest);
return () => map.off("idle", updateNearest);
}, [mapRef, weatherCities]);
const handleProfileVisitIsland = (island) => { const handleProfileVisitIsland = (island) => {
console.log('[MapView] Visiting island from profile:', island.username); console.log('[MapView] Visiting island from profile:', island.username);
setSelectedProfile(null); // Close profile modal setSelectedProfile(null); // Close profile modal
@ -1721,7 +1801,7 @@ export default function MapView({
<div className="map-page"> <div className="map-page">
<div className="map-stage" ref={stageRef}> <div className="map-stage" ref={stageRef}>
<div ref={containerRef} className="map-container" /> <div ref={containerRef} className="map-container" />
<DayNightTerminator map={mapRef.current} enabled={dayNightEnabled} opacity={0.3} /> <DayNightTerminator map={mapRef.current} enabled={weatherViewEnabled} opacity={0.3} />
{status && <div className="map-status">{status}</div>} {status && <div className="map-status">{status}</div>}
{debugEnabled && ( {debugEnabled && (
<> <>
@ -1807,15 +1887,58 @@ export default function MapView({
</button> </button>
<button <button
type="button" type="button"
className={"sw-search__iconBtn" + (dayNightEnabled ? " is-active" : "")} className={"sw-search__iconBtn" + (weatherViewEnabled ? " is-active" : "")}
title="Day/Night View" title="Météo & Ensoleillement"
onClick={() => setDayNightEnabled((prev) => !prev)} onClick={() => setWeatherViewEnabled((prev) => !prev)}
> >
<i className={dayNightEnabled ? "fa-solid fa-moon" : "fa-solid fa-sun"}></i> <i className="fa-solid fa-cloud-sun"></i>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
{/* Weather widget - single nearest city - BELOW search bar */}
{nearestWeather && (() => {
const w = nearestWeather;
const isSunny = w.icon === '☀️' || w.icon === '⛅';
const isNight = w.icon === '🌙';
const isRainy = w.icon === '🌧️' || w.icon === '🌦️' || w.icon === '⛈️';
const isSnow = w.icon === '❄️' || w.icon === '🌨️';
const bg = isNight
? 'linear-gradient(135deg, rgba(30, 41, 59, 0.9), rgba(15, 23, 42, 0.9))'
: isSunny
? 'linear-gradient(135deg, rgba(56, 189, 248, 0.85), rgba(14, 165, 233, 0.85))'
: isRainy
? 'linear-gradient(135deg, rgba(71, 85, 105, 0.9), rgba(51, 65, 85, 0.9))'
: isSnow
? 'linear-gradient(135deg, rgba(148, 163, 184, 0.9), rgba(203, 213, 225, 0.9))'
: 'linear-gradient(135deg, rgba(100, 116, 139, 0.85), rgba(71, 85, 105, 0.85))';
return (
<div
onClick={() => setActiveWeatherPost(w)}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '5px',
marginTop: '8px',
padding: '5px 12px',
borderRadius: '16px',
background: bg,
border: '1px solid rgba(255,255,255,0.25)',
color: isSnow ? '#1e293b' : '#fff',
fontSize: '13px',
fontWeight: '600',
cursor: 'pointer',
backdropFilter: 'blur(8px)',
boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
}}
>
<span style={{ fontSize: '16px' }}>{w.icon}</span>
<span>{w.city}:</span>
<span style={{ fontWeight: '700' }}>{w.temp}°</span>
</div>
);
})()}
</div> </div>
<div className="map-overlay map-overlay-left"> <div className="map-overlay map-overlay-left">

View File

@ -2228,8 +2228,9 @@ export function createWeatherMarkerForPost(post, mapRef, markersRef, expandedElR
const cityDisplay = city || post?.city || ""; const cityDisplay = city || post?.city || "";
const root = document.createElement("div"); const root = document.createElement("div");
root.className = "weather-marker sw-appear"; root.className = "sw-weather-pill sw-appear";
root.style.zIndex = "2"; root.style.zIndex = "5";
root.style.cursor = "pointer";
root.__post = post; root.__post = post;
requestAnimationFrame(() => { requestAnimationFrame(() => {
@ -2237,11 +2238,19 @@ export function createWeatherMarkerForPost(post, mapRef, markersRef, expandedElR
}); });
root.innerHTML = ` root.innerHTML = `
<div class="weather-marker-content"> <div style="
<span class="weather-marker-icon">${emoji}</span> display: flex;
<span class="weather-marker-temp">${tempDisplay}</span> align-items: center;
gap: 3px;
padding: 4px 8px;
background: rgba(14, 165, 233, 0.75);
border-radius: 12px;
border: 1px solid rgba(255,255,255,0.4);
box-shadow: 0 2px 6px rgba(0,0,0,0.25);
">
<span style="font-size:12px;line-height:1;">${emoji}</span>
<span style="font-size:11px;font-weight:600;color:#fff;">${tempDisplay}</span>
</div> </div>
<div class="weather-marker-city">${cityDisplay}</div>
`; `;
// Click to open weather modal // Click to open weather modal
@ -2252,17 +2261,15 @@ export function createWeatherMarkerForPost(post, mapRef, markersRef, expandedElR
} catch {} } catch {}
}); });
// Hover effect // Hover effect - subtle
root.addEventListener("mouseenter", () => { root.addEventListener("mouseenter", () => {
try { try {
root.style.transform = "scale(1.1)"; root.style.opacity = "1";
root.style.zIndex = "100";
} catch {} } catch {}
}); });
root.addEventListener("mouseleave", () => { root.addEventListener("mouseleave", () => {
try { try {
root.style.transform = ""; root.style.opacity = "";
root.style.zIndex = "2";
} catch {} } catch {}
}); });
@ -2387,21 +2394,27 @@ 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()) : '';
// Marqueur pin avec image en haut et pointe en bas // Marqueur pin avec image en haut et pointe en bas
root.innerHTML = ` root.innerHTML = `
<div class="sw-simple-marker ${isCamera ? 'sw-camera-marker' : ''} ${isLive ? 'sw-live-marker' : ''} ${isWeather ? 'sw-weather-marker' : ''}" style=" <div class="sw-simple-marker ${isCamera ? 'sw-camera-marker' : ''} ${isLive ? 'sw-live-marker' : ''} ${isWeather ? 'sw-weather-marker' : ''}" style="
${isWeather ? 'width: auto; height: auto; padding: 6px 10px; border-radius: 16px;' : 'width: 32px; height: 32px; border-radius: 50%;'} ${isWeather ? 'width: auto; height: auto; padding: 4px 8px; border-radius: 12px;' : 'width: 32px; height: 32px; border-radius: 50%;'}
background: ${color}; background: ${isWeather ? 'rgba(14, 165, 233, 0.75)' : color};
border: 2px solid ${isCamera ? 'rgba(255,255,255,0.85)' : isLive ? 'rgba(255,255,255,0.9)' : isWeather ? 'rgba(255,255,255,0.4)' : 'white'}; border: ${isWeather ? '1px solid rgba(255,255,255,0.4)' : isCamera ? '2px solid rgba(255,255,255,0.85)' : isLive ? '2px solid rgba(255,255,255,0.9)' : '2px solid white'};
box-shadow: 0 2px 8px rgba(0,0,0,0.3)${isCamera ? ', 0 0 10px rgba(56, 189, 248, 0.5)' : isLive ? ', 0 0 12px rgba(239,68,68,0.6)' : isWeather ? ', 0 3px 10px rgba(0,0,0,0.4)' : ''}; box-shadow: 0 2px 6px rgba(0,0,0,0.25);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
${isWeather ? 'gap: 4px;' : ''} ${isWeather ? 'gap: 3px;' : ''}
overflow: hidden; overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease; transition: transform 0.2s ease, box-shadow 0.2s ease;
"> ">
${isWeather ? `<span style="font-size:16px;line-height:1;">${weatherEmoji}</span><span style="font-size:13px;font-weight:700;color:#fff;">${weatherTemp}</span>` : isLive ? `<i class="fa-solid fa-broadcast-tower" style="font-size:14px;color:#fff;"></i>` : isCamera ? `<i class="fa-solid fa-video" style="font-size:14px;color:#fff;"></i>` : img ? `<img src="${img}" alt="" style="width:100%;height:100%;object-fit:cover;" />` : ` ${isWeather ? `
<span style="font-size:12px;line-height:1;">${weatherEmoji}</span>
<span style="font-size:11px;font-weight:600;color:#fff;">${weatherTemp}</span>
` : isLive ? `<i class="fa-solid fa-broadcast-tower" style="font-size:14px;color:#fff;"></i>` : isCamera ? `<i class="fa-solid fa-video" style="font-size:14px;color:#fff;"></i>` : img ? `<img src="${img}" alt="" style="width:100%;height:100%;object-fit:cover;" />` : `
<div style=" <div style="
width: 12px; width: 12px;
height: 12px; height: 12px;
@ -2420,7 +2433,7 @@ export function createSimpleMarkerForPost(post, mapRef, markersRef, expandedElRe
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3)); filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));
margin-top: -2px; margin-top: -2px;
z-index: 1; z-index: 1;
"></div>` : `<div style="position:absolute;bottom:-24px;left:50%;transform:translateX(-50%);font-size:13px;font-weight:700;color:#fff;background:rgba(0,0,0,0.85);padding:4px 10px;border-radius:6px;white-space:nowrap;">${escapeHtml(activePost?.city || '')}</div>`} "></div>` : ''}
<div class="sw-simple-hover-card" style=" <div class="sw-simple-hover-card" style="
position: absolute; position: absolute;
bottom: 100%; bottom: 100%;

View File

@ -387,6 +387,7 @@ export function usePostsEngine({
username, username,
debugEnabled = false, debugEnabled = false,
theme = "blue", theme = "blue",
weatherEnabled = false,
}) { }) {
const allPostsRef = useRef([]); const allPostsRef = useRef([]);
const lastFetchRef = useRef({ center: null, radiusKm: null, filterKey: "", timeHours: 0, bounds: null }); const lastFetchRef = useRef({ center: null, radiusKm: null, filterKey: "", timeHours: 0, bounds: null });
@ -756,11 +757,10 @@ export function usePostsEngine({
for (const post of visible) { for (const post of visible) {
const id = getId(post); const id = getId(post);
if (id != null) { if (id != null) {
// Weather posts always simple (blue bubble), never full card // Weather posts: skip completely (shown in top bar widget instead)
const ct = (post?.content_type || "").toLowerCase(); const ct = (post?.content_type || "").toLowerCase();
if (ct === "weather") { if (ct === "weather") {
wanted.set(id, { post, type: 'simple' }); continue; // Skip weather markers - they appear in the search bar widget
continue;
} }
// Tag avec le type de marqueur souhaité // Tag avec le type de marqueur souhaité
const isFullCard = const isFullCard =
@ -831,7 +831,7 @@ export function usePostsEngine({
} }
} }
}, },
[mapRef, markersRef, expandedElRef, theme, searchQuery, forceFullPostId] [mapRef, markersRef, expandedElRef, theme, searchQuery, forceFullPostId, weatherEnabled]
); );
useEffect(() => { useEffect(() => {