Fix Indoor Map button, add Meeting Point functionality, render waypoints
- Fix Indoor Map button being covered by participant panel (z-30) - Implement Set Meeting Point modal with emoji selection - Add waypoint markers rendering on the map - Pass waypoints from room through DualMapView to MapView - Fix TypeScript types for WaypointType 🤖 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
1a996931b5
commit
0a234f902a
|
|
@ -8,6 +8,7 @@ import { useLocationSharing } from '@/hooks/useLocationSharing';
|
||||||
import ParticipantList from '@/components/room/ParticipantList';
|
import ParticipantList from '@/components/room/ParticipantList';
|
||||||
import RoomHeader from '@/components/room/RoomHeader';
|
import RoomHeader from '@/components/room/RoomHeader';
|
||||||
import ShareModal from '@/components/room/ShareModal';
|
import ShareModal from '@/components/room/ShareModal';
|
||||||
|
import MeetingPointModal from '@/components/room/MeetingPointModal';
|
||||||
import type { Participant, ParticipantLocation } from '@/types';
|
import type { Participant, ParticipantLocation } from '@/types';
|
||||||
|
|
||||||
// Dynamic import for map to avoid SSR issues with MapLibre
|
// Dynamic import for map to avoid SSR issues with MapLibre
|
||||||
|
|
@ -27,6 +28,7 @@ export default function RoomPage() {
|
||||||
|
|
||||||
const [showShare, setShowShare] = useState(false);
|
const [showShare, setShowShare] = useState(false);
|
||||||
const [showParticipants, setShowParticipants] = useState(true);
|
const [showParticipants, setShowParticipants] = useState(true);
|
||||||
|
const [showMeetingPoint, setShowMeetingPoint] = useState(false);
|
||||||
const [currentUser, setCurrentUser] = useState<{ name: string; emoji: string } | null>(null);
|
const [currentUser, setCurrentUser] = useState<{ name: string; emoji: string } | null>(null);
|
||||||
const [selectedParticipant, setSelectedParticipant] = useState<Participant | null>(null);
|
const [selectedParticipant, setSelectedParticipant] = useState<Participant | null>(null);
|
||||||
|
|
||||||
|
|
@ -201,6 +203,7 @@ export default function RoomPage() {
|
||||||
{/* Map */}
|
{/* Map */}
|
||||||
<DualMapView
|
<DualMapView
|
||||||
participants={participants}
|
participants={participants}
|
||||||
|
waypoints={waypoints}
|
||||||
currentUserId={currentParticipantId || undefined}
|
currentUserId={currentParticipantId || undefined}
|
||||||
currentLocation={currentLocation}
|
currentLocation={currentLocation}
|
||||||
eventId="38c3"
|
eventId="38c3"
|
||||||
|
|
@ -208,6 +211,9 @@ export default function RoomPage() {
|
||||||
setSelectedParticipant(p);
|
setSelectedParticipant(p);
|
||||||
setShowParticipants(true);
|
setShowParticipants(true);
|
||||||
}}
|
}}
|
||||||
|
onWaypointClick={(w) => {
|
||||||
|
console.log('Waypoint clicked:', w.name);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Participant Panel */}
|
{/* Participant Panel */}
|
||||||
|
|
@ -218,6 +224,7 @@ export default function RoomPage() {
|
||||||
currentUserId={currentUser.name}
|
currentUserId={currentUser.name}
|
||||||
onClose={() => setShowParticipants(false)}
|
onClose={() => setShowParticipants(false)}
|
||||||
onNavigateTo={handleNavigateTo}
|
onNavigateTo={handleNavigateTo}
|
||||||
|
onSetMeetingPoint={() => setShowMeetingPoint(true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -234,6 +241,18 @@ export default function RoomPage() {
|
||||||
{showShare && (
|
{showShare && (
|
||||||
<ShareModal roomSlug={slug} onClose={() => setShowShare(false)} />
|
<ShareModal roomSlug={slug} onClose={() => setShowShare(false)} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Meeting Point Modal */}
|
||||||
|
{showMeetingPoint && (
|
||||||
|
<MeetingPointModal
|
||||||
|
currentLocation={currentLocation}
|
||||||
|
onClose={() => setShowMeetingPoint(false)}
|
||||||
|
onSetMeetingPoint={(waypoint) => {
|
||||||
|
addWaypoint(waypoint);
|
||||||
|
setShowMeetingPoint(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import type { Participant, MapViewport } from '@/types';
|
import type { Participant, MapViewport, Waypoint } from '@/types';
|
||||||
import { isInC3NavArea } from '@/lib/c3nav';
|
import { isInC3NavArea } from '@/lib/c3nav';
|
||||||
|
|
||||||
// Dynamic imports to avoid SSR issues
|
// Dynamic imports to avoid SSR issues
|
||||||
|
|
@ -28,11 +28,13 @@ type MapMode = 'outdoor' | 'indoor' | 'auto';
|
||||||
|
|
||||||
interface DualMapViewProps {
|
interface DualMapViewProps {
|
||||||
participants: Participant[];
|
participants: Participant[];
|
||||||
|
waypoints?: Waypoint[];
|
||||||
currentUserId?: string;
|
currentUserId?: string;
|
||||||
currentLocation?: { latitude: number; longitude: number } | null;
|
currentLocation?: { latitude: number; longitude: number } | null;
|
||||||
eventId?: string;
|
eventId?: string;
|
||||||
initialMode?: MapMode;
|
initialMode?: MapMode;
|
||||||
onParticipantClick?: (participant: Participant) => void;
|
onParticipantClick?: (participant: Participant) => void;
|
||||||
|
onWaypointClick?: (waypoint: Waypoint) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CCC venue bounds (Hamburg Congress Center)
|
// CCC venue bounds (Hamburg Congress Center)
|
||||||
|
|
@ -45,11 +47,13 @@ const CCC_BOUNDS = {
|
||||||
|
|
||||||
export default function DualMapView({
|
export default function DualMapView({
|
||||||
participants,
|
participants,
|
||||||
|
waypoints = [],
|
||||||
currentUserId,
|
currentUserId,
|
||||||
currentLocation,
|
currentLocation,
|
||||||
eventId = '38c3',
|
eventId = '38c3',
|
||||||
initialMode = 'auto',
|
initialMode = 'auto',
|
||||||
onParticipantClick,
|
onParticipantClick,
|
||||||
|
onWaypointClick,
|
||||||
}: DualMapViewProps) {
|
}: DualMapViewProps) {
|
||||||
const [mode, setMode] = useState<MapMode>(initialMode);
|
const [mode, setMode] = useState<MapMode>(initialMode);
|
||||||
const [activeView, setActiveView] = useState<'outdoor' | 'indoor'>('outdoor');
|
const [activeView, setActiveView] = useState<'outdoor' | 'indoor'>('outdoor');
|
||||||
|
|
@ -86,8 +90,10 @@ export default function DualMapView({
|
||||||
{activeView === 'outdoor' ? (
|
{activeView === 'outdoor' ? (
|
||||||
<MapView
|
<MapView
|
||||||
participants={participants}
|
participants={participants}
|
||||||
|
waypoints={waypoints}
|
||||||
currentUserId={currentUserId}
|
currentUserId={currentUserId}
|
||||||
onParticipantClick={onParticipantClick}
|
onParticipantClick={onParticipantClick}
|
||||||
|
onWaypointClick={onWaypointClick}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<C3NavEmbed
|
<C3NavEmbed
|
||||||
|
|
@ -103,7 +109,7 @@ export default function DualMapView({
|
||||||
{activeView === 'outdoor' && (
|
{activeView === 'outdoor' && (
|
||||||
<button
|
<button
|
||||||
onClick={goIndoor}
|
onClick={goIndoor}
|
||||||
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 z-10"
|
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 z-30"
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
|
|
@ -126,7 +132,7 @@ export default function DualMapView({
|
||||||
|
|
||||||
{/* Venue proximity indicator */}
|
{/* Venue proximity indicator */}
|
||||||
{currentLocation && isInC3NavArea(currentLocation.latitude, currentLocation.longitude) && activeView === 'outdoor' && (
|
{currentLocation && isInC3NavArea(currentLocation.latitude, currentLocation.longitude) && activeView === 'outdoor' && (
|
||||||
<div className="absolute top-4 left-1/2 -translate-x-1/2 bg-rmaps-secondary/90 text-white text-sm px-3 py-1.5 rounded-full flex items-center gap-2 z-10">
|
<div className="absolute top-4 left-1/2 -translate-x-1/2 bg-rmaps-secondary/90 text-white text-sm px-3 py-1.5 rounded-full flex items-center gap-2 z-30">
|
||||||
<span>You're at the venue!</span>
|
<span>You're at the venue!</span>
|
||||||
<button onClick={goIndoor} className="underline">
|
<button onClick={goIndoor} className="underline">
|
||||||
Switch to indoor
|
Switch to indoor
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,16 @@
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import maplibregl from 'maplibre-gl';
|
import maplibregl from 'maplibre-gl';
|
||||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||||
import type { Participant, MapViewport } from '@/types';
|
import type { Participant, MapViewport, Waypoint } from '@/types';
|
||||||
import FriendMarker from './FriendMarker';
|
import FriendMarker from './FriendMarker';
|
||||||
|
|
||||||
interface MapViewProps {
|
interface MapViewProps {
|
||||||
participants: Participant[];
|
participants: Participant[];
|
||||||
|
waypoints?: Waypoint[];
|
||||||
currentUserId?: string;
|
currentUserId?: string;
|
||||||
initialViewport?: MapViewport;
|
initialViewport?: MapViewport;
|
||||||
onParticipantClick?: (participant: Participant) => void;
|
onParticipantClick?: (participant: Participant) => void;
|
||||||
|
onWaypointClick?: (waypoint: Waypoint) => void;
|
||||||
onMapClick?: (lngLat: { lng: number; lat: number }) => void;
|
onMapClick?: (lngLat: { lng: number; lat: number }) => void;
|
||||||
/** Auto-center on current user's location when first available */
|
/** Auto-center on current user's location when first available */
|
||||||
autoCenterOnUser?: boolean;
|
autoCenterOnUser?: boolean;
|
||||||
|
|
@ -24,15 +26,18 @@ const DEFAULT_VIEWPORT: MapViewport = {
|
||||||
|
|
||||||
export default function MapView({
|
export default function MapView({
|
||||||
participants,
|
participants,
|
||||||
|
waypoints = [],
|
||||||
currentUserId,
|
currentUserId,
|
||||||
initialViewport = DEFAULT_VIEWPORT,
|
initialViewport = DEFAULT_VIEWPORT,
|
||||||
onParticipantClick,
|
onParticipantClick,
|
||||||
|
onWaypointClick,
|
||||||
onMapClick,
|
onMapClick,
|
||||||
autoCenterOnUser = true,
|
autoCenterOnUser = true,
|
||||||
}: MapViewProps) {
|
}: MapViewProps) {
|
||||||
const mapContainer = useRef<HTMLDivElement>(null);
|
const mapContainer = useRef<HTMLDivElement>(null);
|
||||||
const map = useRef<maplibregl.Map | null>(null);
|
const map = useRef<maplibregl.Map | null>(null);
|
||||||
const markersRef = useRef<Map<string, maplibregl.Marker>>(new Map());
|
const markersRef = useRef<Map<string, maplibregl.Marker>>(new Map());
|
||||||
|
const waypointMarkersRef = useRef<Map<string, maplibregl.Marker>>(new Map());
|
||||||
const [mapLoaded, setMapLoaded] = useState(false);
|
const [mapLoaded, setMapLoaded] = useState(false);
|
||||||
const hasCenteredOnUserRef = useRef(false);
|
const hasCenteredOnUserRef = useRef(false);
|
||||||
|
|
||||||
|
|
@ -47,13 +52,15 @@ export default function MapView({
|
||||||
sources: {
|
sources: {
|
||||||
osm: {
|
osm: {
|
||||||
type: 'raster',
|
type: 'raster',
|
||||||
|
// Use Carto's basemaps - more reliable than direct OSM tiles
|
||||||
tiles: [
|
tiles: [
|
||||||
'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
'https://a.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}@2x.png',
|
||||||
'https://b.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
'https://b.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}@2x.png',
|
||||||
'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
'https://c.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}@2x.png',
|
||||||
|
'https://d.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}@2x.png',
|
||||||
],
|
],
|
||||||
tileSize: 256,
|
tileSize: 256,
|
||||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/attributions">CARTO</a>',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
layers: [
|
layers: [
|
||||||
|
|
@ -62,7 +69,7 @@ export default function MapView({
|
||||||
type: 'raster',
|
type: 'raster',
|
||||||
source: 'osm',
|
source: 'osm',
|
||||||
minzoom: 0,
|
minzoom: 0,
|
||||||
maxzoom: 19,
|
maxzoom: 20,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -172,6 +179,94 @@ export default function MapView({
|
||||||
}
|
}
|
||||||
}, [participants, mapLoaded, currentUserId, onParticipantClick, autoCenterOnUser]);
|
}, [participants, mapLoaded, currentUserId, onParticipantClick, autoCenterOnUser]);
|
||||||
|
|
||||||
|
// Update waypoint markers
|
||||||
|
useEffect(() => {
|
||||||
|
if (!map.current || !mapLoaded) return;
|
||||||
|
|
||||||
|
const currentWaypointMarkers = waypointMarkersRef.current;
|
||||||
|
const waypointIds = new Set(waypoints.map((w) => w.id));
|
||||||
|
|
||||||
|
// Remove markers for deleted waypoints
|
||||||
|
currentWaypointMarkers.forEach((marker, id) => {
|
||||||
|
if (!waypointIds.has(id)) {
|
||||||
|
marker.remove();
|
||||||
|
currentWaypointMarkers.delete(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add/update waypoint markers
|
||||||
|
waypoints.forEach((waypoint) => {
|
||||||
|
const { latitude, longitude } = waypoint.location;
|
||||||
|
let marker = currentWaypointMarkers.get(waypoint.id);
|
||||||
|
|
||||||
|
if (marker) {
|
||||||
|
// Update position if changed
|
||||||
|
marker.setLngLat([longitude, latitude]);
|
||||||
|
} else {
|
||||||
|
// Create new waypoint marker
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'waypoint-marker';
|
||||||
|
el.innerHTML = `
|
||||||
|
<div class="waypoint-icon">${waypoint.emoji || '📍'}</div>
|
||||||
|
<div class="waypoint-label">${waypoint.name}</div>
|
||||||
|
`;
|
||||||
|
el.style.cssText = `
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const iconDiv = el.querySelector('.waypoint-icon') as HTMLElement;
|
||||||
|
if (iconDiv) {
|
||||||
|
iconDiv.style.cssText = `
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||||
|
border-radius: 50% 50% 50% 0;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||||
|
`;
|
||||||
|
const innerSpan = document.createElement('span');
|
||||||
|
innerSpan.style.cssText = 'transform: rotate(45deg); font-size: 14px;';
|
||||||
|
innerSpan.textContent = waypoint.emoji || '📍';
|
||||||
|
iconDiv.innerHTML = '';
|
||||||
|
iconDiv.appendChild(innerSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelDiv = el.querySelector('.waypoint-label') as HTMLElement;
|
||||||
|
if (labelDiv) {
|
||||||
|
labelDiv.style.cssText = `
|
||||||
|
background: rgba(0,0,0,0.8);
|
||||||
|
color: white;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 120px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onWaypointClick?.(waypoint);
|
||||||
|
});
|
||||||
|
|
||||||
|
marker = new maplibregl.Marker({ element: el })
|
||||||
|
.setLngLat([longitude, latitude])
|
||||||
|
.addTo(map.current!);
|
||||||
|
|
||||||
|
currentWaypointMarkers.set(waypoint.id, marker);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [waypoints, mapLoaded, onWaypointClick]);
|
||||||
|
|
||||||
// Fit bounds to show all participants
|
// Fit bounds to show all participants
|
||||||
const fitToParticipants = () => {
|
const fitToParticipants = () => {
|
||||||
if (!map.current || participants.length === 0) return;
|
if (!map.current || participants.length === 0) return;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import type { ParticipantLocation, WaypointType } from '@/types';
|
||||||
|
|
||||||
|
interface MeetingPointModalProps {
|
||||||
|
currentLocation?: ParticipantLocation | null;
|
||||||
|
onClose: () => void;
|
||||||
|
onSetMeetingPoint: (waypoint: {
|
||||||
|
name: string;
|
||||||
|
emoji: string;
|
||||||
|
location: { latitude: number; longitude: number };
|
||||||
|
type: WaypointType;
|
||||||
|
}) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMOJI_OPTIONS = ['📍', '🎯', '🏁', '⭐', '🍺', '☕', '🍕', '🎪', '🚻', '🚪'];
|
||||||
|
|
||||||
|
export default function MeetingPointModal({
|
||||||
|
currentLocation,
|
||||||
|
onClose,
|
||||||
|
onSetMeetingPoint,
|
||||||
|
}: MeetingPointModalProps) {
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [emoji, setEmoji] = useState('📍');
|
||||||
|
const [useCurrentLocation, setUseCurrentLocation] = useState(true);
|
||||||
|
const [customLat, setCustomLat] = useState('');
|
||||||
|
const [customLng, setCustomLng] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentLocation) {
|
||||||
|
setCustomLat(currentLocation.latitude.toFixed(6));
|
||||||
|
setCustomLng(currentLocation.longitude.toFixed(6));
|
||||||
|
}
|
||||||
|
}, [currentLocation]);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
let latitude: number;
|
||||||
|
let longitude: number;
|
||||||
|
|
||||||
|
if (useCurrentLocation && currentLocation) {
|
||||||
|
latitude = currentLocation.latitude;
|
||||||
|
longitude = currentLocation.longitude;
|
||||||
|
} else {
|
||||||
|
latitude = parseFloat(customLat);
|
||||||
|
longitude = parseFloat(customLng);
|
||||||
|
if (isNaN(latitude) || isNaN(longitude)) {
|
||||||
|
alert('Please enter valid coordinates');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSetMeetingPoint({
|
||||||
|
name: name || 'Meeting Point',
|
||||||
|
emoji,
|
||||||
|
location: { latitude, longitude },
|
||||||
|
type: 'meetup',
|
||||||
|
});
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasLocation = currentLocation || (!isNaN(parseFloat(customLat)) && !isNaN(parseFloat(customLng)));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
||||||
|
<div className="room-panel rounded-2xl p-6 w-full max-w-md">
|
||||||
|
<h2 className="text-xl font-bold mb-4">Set Meeting Point</h2>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
{/* Name input */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-white/60 mb-1">Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="e.g., Main entrance, Food court..."
|
||||||
|
className="w-full bg-white/10 border border-white/20 rounded-lg px-3 py-2 text-white placeholder:text-white/40 focus:outline-none focus:border-rmaps-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Emoji selector */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-white/60 mb-1">Icon</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{EMOJI_OPTIONS.map((e) => (
|
||||||
|
<button
|
||||||
|
key={e}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setEmoji(e)}
|
||||||
|
className={`w-10 h-10 rounded-lg text-xl flex items-center justify-center transition-colors ${
|
||||||
|
emoji === e
|
||||||
|
? 'bg-rmaps-primary'
|
||||||
|
: 'bg-white/10 hover:bg-white/20'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{e}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Location options */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-white/60 mb-2">Location</label>
|
||||||
|
|
||||||
|
{currentLocation && (
|
||||||
|
<label className="flex items-center gap-2 mb-3 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
checked={useCurrentLocation}
|
||||||
|
onChange={() => setUseCurrentLocation(true)}
|
||||||
|
className="accent-rmaps-primary"
|
||||||
|
/>
|
||||||
|
<span className="text-sm">Use my current location</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
checked={!useCurrentLocation}
|
||||||
|
onChange={() => setUseCurrentLocation(false)}
|
||||||
|
className="accent-rmaps-primary"
|
||||||
|
/>
|
||||||
|
<span className="text-sm">Enter coordinates manually</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{!useCurrentLocation && (
|
||||||
|
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-white/40 mb-1">Latitude</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={customLat}
|
||||||
|
onChange={(e) => setCustomLat(e.target.value)}
|
||||||
|
placeholder="53.5550"
|
||||||
|
className="w-full bg-white/10 border border-white/20 rounded-lg px-3 py-2 text-white text-sm placeholder:text-white/40 focus:outline-none focus:border-rmaps-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-white/40 mb-1">Longitude</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={customLng}
|
||||||
|
onChange={(e) => setCustomLng(e.target.value)}
|
||||||
|
placeholder="9.9898"
|
||||||
|
className="w-full bg-white/10 border border-white/20 rounded-lg px-3 py-2 text-white text-sm placeholder:text-white/40 focus:outline-none focus:border-rmaps-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!hasLocation && !useCurrentLocation && (
|
||||||
|
<p className="text-xs text-yellow-400 mt-2">
|
||||||
|
Share your location first, or enter coordinates manually
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="btn-ghost flex-1"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn-primary flex-1"
|
||||||
|
disabled={!hasLocation && useCurrentLocation}
|
||||||
|
>
|
||||||
|
Set Point
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ interface ParticipantListProps {
|
||||||
currentUserId?: string;
|
currentUserId?: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onNavigateTo: (participant: Participant) => void;
|
onNavigateTo: (participant: Participant) => void;
|
||||||
|
onSetMeetingPoint?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ParticipantList({
|
export default function ParticipantList({
|
||||||
|
|
@ -14,6 +15,7 @@ export default function ParticipantList({
|
||||||
currentUserId,
|
currentUserId,
|
||||||
onClose,
|
onClose,
|
||||||
onNavigateTo,
|
onNavigateTo,
|
||||||
|
onSetMeetingPoint,
|
||||||
}: ParticipantListProps) {
|
}: ParticipantListProps) {
|
||||||
const formatDistance = (participant: Participant, current: Participant | undefined) => {
|
const formatDistance = (participant: Participant, current: Participant | undefined) => {
|
||||||
if (!participant.location || !current?.location) return null;
|
if (!participant.location || !current?.location) return null;
|
||||||
|
|
@ -142,7 +144,10 @@ export default function ParticipantList({
|
||||||
|
|
||||||
{/* Footer actions */}
|
{/* Footer actions */}
|
||||||
<div className="p-4 border-t border-white/10">
|
<div className="p-4 border-t border-white/10">
|
||||||
<button className="btn-secondary w-full text-sm">
|
<button
|
||||||
|
className="btn-secondary w-full text-sm"
|
||||||
|
onClick={onSetMeetingPoint}
|
||||||
|
>
|
||||||
Set Meeting Point
|
Set Meeting Point
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue