feat: Direct room URL navigation + fix c3nav iframe blocking
- Add inline JoinForm when navigating directly to room URL - No longer redirects to home page for new users - Replace c3nav iframe with fallback UI (c3nav blocks iframe embedding) - Add 'Open c3nav' button to open indoor map in new tab - Still shows friend markers with indoor locations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
908767be15
commit
796fd2c727
|
|
@ -13,6 +13,7 @@ import ShareModal from '@/components/room/ShareModal';
|
|||
import MeetingPointModal from '@/components/room/MeetingPointModal';
|
||||
import WaypointModal from '@/components/room/WaypointModal';
|
||||
import InstallBanner from '@/components/room/InstallBanner';
|
||||
import JoinForm from '@/components/room/JoinForm';
|
||||
import type { Participant, ParticipantLocation, Waypoint } from '@/types';
|
||||
|
||||
// Dynamic import for map to avoid SSR issues with MapLibre
|
||||
|
|
@ -38,24 +39,31 @@ export default function RoomPage() {
|
|||
const [selectedWaypoint, setSelectedWaypoint] = useState<Waypoint | null>(null);
|
||||
const [shouldAutoStartSharing, setShouldAutoStartSharing] = useState(false);
|
||||
const [zoomToLocation, setZoomToLocation] = useState<{ latitude: number; longitude: number } | null>(null);
|
||||
const [needsJoin, setNeedsJoin] = useState(false);
|
||||
|
||||
// Load user and sharing preference from localStorage
|
||||
useEffect(() => {
|
||||
const stored = localStorage.getItem('rmaps_user');
|
||||
if (stored) {
|
||||
setCurrentUser(JSON.parse(stored));
|
||||
} else {
|
||||
// Redirect to home if no user info
|
||||
router.push('/');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user had location sharing enabled for this room
|
||||
const sharingPref = localStorage.getItem(`rmaps_sharing_${slug}`);
|
||||
if (sharingPref === 'true') {
|
||||
setShouldAutoStartSharing(true);
|
||||
}
|
||||
}, [router, slug]);
|
||||
} else {
|
||||
// Show join form instead of redirecting
|
||||
setNeedsJoin(true);
|
||||
}
|
||||
}, [slug]);
|
||||
|
||||
// Handle joining from the inline form
|
||||
const handleJoin = (name: string, emoji: string) => {
|
||||
const user = { name, emoji };
|
||||
localStorage.setItem('rmaps_user', JSON.stringify(user));
|
||||
setCurrentUser(user);
|
||||
setNeedsJoin(false);
|
||||
};
|
||||
|
||||
// Room hook (only initialize when we have user info)
|
||||
const {
|
||||
|
|
@ -276,6 +284,11 @@ export default function RoomPage() {
|
|||
setShowParticipants(false); // Close participant list to show map
|
||||
};
|
||||
|
||||
// Show join form if user hasn't entered their name yet
|
||||
if (needsJoin) {
|
||||
return <JoinForm roomSlug={slug} onJoin={handleJoin} />;
|
||||
}
|
||||
|
||||
// Loading state
|
||||
if (!currentUser || isLoading) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,19 +1,14 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import type { Participant } from '@/types';
|
||||
|
||||
interface C3NavEmbedProps {
|
||||
/** Event identifier (e.g., '39c3', 'eh2025') */
|
||||
eventId?: string;
|
||||
/** Initial location to show */
|
||||
initialLocation?: string;
|
||||
/** Participants to show on the map overlay */
|
||||
participants?: Participant[];
|
||||
/** Current user ID */
|
||||
currentUserId?: string;
|
||||
/** Callback when user taps a location */
|
||||
onLocationSelect?: (location: { slug: string; name: string }) => void;
|
||||
/** Show the indoor/outdoor toggle */
|
||||
showToggle?: boolean;
|
||||
/** Callback when toggling to outdoor mode */
|
||||
|
|
@ -32,105 +27,37 @@ const C3NAV_EVENTS: Record<string, string> = {
|
|||
|
||||
export default function C3NavEmbed({
|
||||
eventId = '39c3',
|
||||
initialLocation,
|
||||
participants = [],
|
||||
currentUserId,
|
||||
onLocationSelect,
|
||||
showToggle = true,
|
||||
onToggleOutdoor,
|
||||
}: C3NavEmbedProps) {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Get the c3nav base URL for the event
|
||||
const baseUrl = C3NAV_EVENTS[eventId] || C3NAV_EVENTS['39c3'];
|
||||
|
||||
// Build the embed URL
|
||||
const embedUrl = new URL(baseUrl);
|
||||
embedUrl.searchParams.set('embed', '1');
|
||||
if (initialLocation) {
|
||||
embedUrl.searchParams.set('o', initialLocation);
|
||||
}
|
||||
|
||||
// Handle iframe load
|
||||
const handleLoad = () => {
|
||||
setIsLoading(false);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
// Handle iframe error
|
||||
const handleError = () => {
|
||||
setIsLoading(false);
|
||||
setError('Failed to load indoor map');
|
||||
};
|
||||
|
||||
// Listen for messages from c3nav iframe
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
// Only accept messages from c3nav
|
||||
if (!event.origin.includes('c3nav.de')) return;
|
||||
|
||||
try {
|
||||
const data = event.data;
|
||||
if (data.type === 'c3nav:location' && onLocationSelect) {
|
||||
onLocationSelect({
|
||||
slug: data.slug,
|
||||
name: data.name,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore invalid messages
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handleMessage);
|
||||
return () => window.removeEventListener('message', handleMessage);
|
||||
}, [onLocationSelect]);
|
||||
|
||||
// c3nav blocks iframes, so show fallback UI with link to open in new tab
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
{/* c3nav iframe */}
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={embedUrl.toString()}
|
||||
className="w-full h-full border-0"
|
||||
allow="geolocation"
|
||||
onLoad={handleLoad}
|
||||
onError={handleError}
|
||||
title="c3nav indoor navigation"
|
||||
/>
|
||||
|
||||
{/* Loading overlay */}
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 bg-rmaps-dark flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-8 h-8 border-2 border-rmaps-primary border-t-transparent rounded-full animate-spin mx-auto mb-2" />
|
||||
<div className="text-white/60 text-sm">Loading indoor map...</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error overlay */}
|
||||
{error && (
|
||||
<div className="absolute inset-0 bg-rmaps-dark flex items-center justify-center">
|
||||
<div className="text-center p-4">
|
||||
<div className="text-red-400 mb-2">{error}</div>
|
||||
<div className="relative w-full h-full bg-rmaps-dark">
|
||||
{/* Fallback UI since c3nav blocks iframe embedding */}
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center p-6 text-center">
|
||||
<div className="text-6xl mb-4">🏢</div>
|
||||
<h3 className="text-xl font-semibold text-white mb-2">Indoor Navigation</h3>
|
||||
<p className="text-white/60 text-sm mb-6 max-w-xs">
|
||||
Open c3nav to view the indoor map and navigate the venue
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
if (iframeRef.current) {
|
||||
iframeRef.current.src = embedUrl.toString();
|
||||
}
|
||||
}}
|
||||
className="btn-ghost text-sm"
|
||||
onClick={() => window.open(baseUrl, '_blank')}
|
||||
className="btn-primary flex items-center gap-2"
|
||||
>
|
||||
Retry
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
Open c3nav
|
||||
</button>
|
||||
<p className="text-white/40 text-xs mt-4">
|
||||
{eventId.toUpperCase()} venue map
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Friend markers overlay */}
|
||||
{participants.length > 0 && (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
const EMOJIS = ['😀', '😎', '🤓', '🥳', '🦊', '🐱', '🐶', '🦄', '🌟', '🔥', '💜', '🎮'];
|
||||
|
||||
interface JoinFormProps {
|
||||
roomSlug: string;
|
||||
onJoin: (name: string, emoji: string) => void;
|
||||
}
|
||||
|
||||
export default function JoinForm({ roomSlug, onJoin }: JoinFormProps) {
|
||||
const [name, setName] = useState('');
|
||||
const [emoji, setEmoji] = useState(EMOJIS[Math.floor(Math.random() * EMOJIS.length)]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (name.trim()) {
|
||||
onJoin(name.trim(), emoji);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-4 bg-rmaps-dark">
|
||||
<div className="room-panel rounded-2xl p-6 max-w-sm w-full">
|
||||
<div className="text-center mb-6">
|
||||
<h1 className="text-2xl font-bold text-white mb-2">Join Room</h1>
|
||||
<p className="text-white/60 text-sm">
|
||||
You're joining <span className="text-rmaps-primary font-medium">/{roomSlug}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Emoji picker */}
|
||||
<div>
|
||||
<label className="block text-white/60 text-sm mb-2">Pick your avatar</label>
|
||||
<div className="flex flex-wrap gap-2 justify-center">
|
||||
{EMOJIS.map((e) => (
|
||||
<button
|
||||
key={e}
|
||||
type="button"
|
||||
onClick={() => setEmoji(e)}
|
||||
className={`w-10 h-10 rounded-full text-xl flex items-center justify-center transition-all ${
|
||||
emoji === e
|
||||
? 'bg-rmaps-primary scale-110'
|
||||
: 'bg-white/10 hover:bg-white/20'
|
||||
}`}
|
||||
>
|
||||
{e}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Name input */}
|
||||
<div>
|
||||
<label className="block text-white/60 text-sm mb-2">Your name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Enter your name"
|
||||
className="w-full px-4 py-3 rounded-lg bg-white/10 border border-white/20 text-white placeholder-white/40 focus:border-rmaps-primary focus:outline-none"
|
||||
maxLength={20}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
<div className="flex items-center justify-center gap-3 py-3 bg-white/5 rounded-lg">
|
||||
<div className="w-12 h-12 rounded-full bg-rmaps-primary/30 flex items-center justify-center text-2xl">
|
||||
{emoji}
|
||||
</div>
|
||||
<span className="text-white font-medium">
|
||||
{name || 'Your Name'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!name.trim()}
|
||||
className="w-full btn-primary py-3 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Join Room
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Location sharing note */}
|
||||
<p className="text-white/40 text-xs text-center mt-4">
|
||||
Location sharing is optional and can be toggled anytime
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue