rmaps-online/src/components/map/C3NavEmbed.tsx

190 lines
5.9 KiB
TypeScript

'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 */
onToggleOutdoor?: () => void;
}
// c3nav event URLs
const C3NAV_EVENTS: Record<string, string> = {
'39c3': 'https://39c3.c3nav.de',
'38c3': 'https://38c3.c3nav.de',
'37c3': 'https://37c3.c3nav.de',
'eh2025': 'https://eh2025.c3nav.de',
'eh22': 'https://eh22.c3nav.de',
'camp2023': 'https://camp2023.c3nav.de',
};
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]);
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>
<button
onClick={() => {
setIsLoading(true);
setError(null);
if (iframeRef.current) {
iframeRef.current.src = embedUrl.toString();
}
}}
className="btn-ghost text-sm"
>
Retry
</button>
</div>
</div>
)}
{/* Friend markers overlay */}
{participants.length > 0 && (
<div className="absolute top-2 left-2 right-2 pointer-events-none">
<div className="flex flex-wrap gap-1 pointer-events-auto">
{participants
.filter((p) => p.location?.indoor && p.id !== currentUserId)
.map((p) => (
<div
key={p.id}
className="flex items-center gap-1 bg-rmaps-dark/90 rounded-full px-2 py-1 text-xs"
style={{ borderColor: p.color, borderWidth: 2 }}
>
<span>{p.emoji}</span>
<span className="text-white/80">{p.name}</span>
{p.location?.indoor?.spaceName && (
<span className="text-white/50">@ {p.location.indoor.spaceName}</span>
)}
</div>
))}
</div>
</div>
)}
{/* Toggle to outdoor */}
{showToggle && onToggleOutdoor && (
<button
onClick={onToggleOutdoor}
className="absolute bottom-4 left-4 bg-rmaps-dark/90 text-white px-3 py-2 rounded-lg text-sm hover:bg-rmaps-dark transition-colors flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
Outdoor Map
</button>
)}
{/* c3nav attribution */}
<div className="absolute bottom-4 right-4 text-xs text-white/40">
<a
href={baseUrl}
target="_blank"
rel="noopener noreferrer"
className="hover:text-white/60"
>
Powered by c3nav
</a>
</div>
</div>
);
}