From 50e1feb62c5b8c3dc5c921efd2f61843ca4eb668 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 15 Dec 2025 16:38:14 -0500 Subject: [PATCH] Improve Meeting Point UX: add address search, fix Indoor Map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Indoor Map now opens c3nav in new tab (iframe blocked by X-Frame-Options) - Meeting Point modal now has: - "Use My Location" button that fetches GPS position on demand - Address search using OpenStreetMap Nominatim API - Search results dropdown with clickable options - Manual coordinates entry (hidden by default) - Selected location preview with coordinates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/components/map/DualMapView.tsx | 13 +- src/components/room/MeetingPointModal.tsx | 243 ++++++++++++++++++---- 2 files changed, 211 insertions(+), 45 deletions(-) diff --git a/src/components/map/DualMapView.tsx b/src/components/map/DualMapView.tsx index a570366..3a2097a 100644 --- a/src/components/map/DualMapView.tsx +++ b/src/components/map/DualMapView.tsx @@ -105,10 +105,12 @@ export default function DualMapView({ /> )} - {/* Mode toggle (when outdoor) */} + {/* Indoor Map button - opens c3nav in new tab (iframe embedding blocked) */} {activeView === 'outdoor' && ( - + + + + )} {/* Auto-mode indicator */} diff --git a/src/components/room/MeetingPointModal.tsx b/src/components/room/MeetingPointModal.tsx index db37a87..86bca83 100644 --- a/src/components/room/MeetingPointModal.tsx +++ b/src/components/room/MeetingPointModal.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import type { ParticipantLocation, WaypointType } from '@/types'; interface MeetingPointModalProps { @@ -14,6 +14,13 @@ interface MeetingPointModalProps { }) => void; } +interface SearchResult { + place_id: number; + display_name: string; + lat: string; + lon: string; +} + const EMOJI_OPTIONS = ['📍', '🎯', '🏁', '⭐', '🍺', '☕', '🍕', '🎪', '🚻', '🚪']; export default function MeetingPointModal({ @@ -23,37 +30,122 @@ export default function MeetingPointModal({ }: MeetingPointModalProps) { const [name, setName] = useState(''); const [emoji, setEmoji] = useState('📍'); - const [useCurrentLocation, setUseCurrentLocation] = useState(true); + const [locationMode, setLocationMode] = useState<'current' | 'search' | 'manual'>('search'); + const [searchQuery, setSearchQuery] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const [selectedLocation, setSelectedLocation] = useState<{ lat: number; lng: number; name?: string } | null>(null); + const [isGettingLocation, setIsGettingLocation] = useState(false); + const [locationError, setLocationError] = useState(null); const [customLat, setCustomLat] = useState(''); const [customLng, setCustomLng] = useState(''); + // Use current location if available useEffect(() => { if (currentLocation) { - setCustomLat(currentLocation.latitude.toFixed(6)); - setCustomLng(currentLocation.longitude.toFixed(6)); + setSelectedLocation({ + lat: currentLocation.latitude, + lng: currentLocation.longitude, + name: 'My current location', + }); } }, [currentLocation]); + // Search for addresses using Nominatim + const searchAddress = useCallback(async () => { + if (!searchQuery.trim()) return; + + setIsSearching(true); + try { + const response = await fetch( + `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(searchQuery)}&limit=5`, + { + headers: { + 'User-Agent': 'rMaps.online/1.0', + }, + } + ); + const data: SearchResult[] = await response.json(); + setSearchResults(data); + } catch (error) { + console.error('Search failed:', error); + setSearchResults([]); + } finally { + setIsSearching(false); + } + }, [searchQuery]); + + // Get current location on demand + const getCurrentLocation = useCallback(() => { + if (!('geolocation' in navigator)) { + setLocationError('Geolocation is not supported by your browser'); + return; + } + + setIsGettingLocation(true); + setLocationError(null); + + navigator.geolocation.getCurrentPosition( + (position) => { + setSelectedLocation({ + lat: position.coords.latitude, + lng: position.coords.longitude, + name: 'My current location', + }); + setIsGettingLocation(false); + setLocationMode('current'); + }, + (error) => { + setLocationError( + error.code === 1 + ? 'Location permission denied' + : error.code === 2 + ? 'Location unavailable' + : 'Location request timed out' + ); + setIsGettingLocation(false); + }, + { + enableHighAccuracy: true, + timeout: 15000, + maximumAge: 0, + } + ); + }, []); + + const selectSearchResult = (result: SearchResult) => { + setSelectedLocation({ + lat: parseFloat(result.lat), + lng: parseFloat(result.lon), + name: result.display_name.split(',')[0], + }); + setSearchResults([]); + setSearchQuery(result.display_name.split(',')[0]); + }; + const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); let latitude: number; let longitude: number; - if (useCurrentLocation && currentLocation) { - latitude = currentLocation.latitude; - longitude = currentLocation.longitude; - } else { + if (locationMode === 'manual') { latitude = parseFloat(customLat); longitude = parseFloat(customLng); if (isNaN(latitude) || isNaN(longitude)) { - alert('Please enter valid coordinates'); + setLocationError('Please enter valid coordinates'); return; } + } else if (selectedLocation) { + latitude = selectedLocation.lat; + longitude = selectedLocation.lng; + } else { + setLocationError('Please select a location'); + return; } onSetMeetingPoint({ - name: name || 'Meeting Point', + name: name || selectedLocation?.name || 'Meeting Point', emoji, location: { latitude, longitude }, type: 'meetup', @@ -62,17 +154,15 @@ export default function MeetingPointModal({ onClose(); }; - const hasLocation = currentLocation || (!isNaN(parseFloat(customLat)) && !isNaN(parseFloat(customLng))); - return (
-
+

Set Meeting Point

{/* Name input */}
- +
- {/* Location options */} + {/* Location selection */}
- {currentLocation && ( - + {/* Quick actions */} +
+ +
+ + {/* Search input */} +
+ { + setSearchQuery(e.target.value); + setLocationMode('search'); + }} + onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), searchAddress())} + placeholder="Search for an address..." + className="w-full bg-white/10 border border-white/20 rounded-lg px-3 py-2 pr-10 text-white placeholder:text-white/40 focus:outline-none focus:border-rmaps-primary" + /> + +
+ + {/* Search results */} + {searchResults.length > 0 && ( +
+ {searchResults.map((result) => ( + + ))} +
)} - + {/* Manual coordinates toggle */} + - {!useCurrentLocation && ( -
+ {/* Manual coordinates input */} + {locationMode === 'manual' && ( +
)} - {!hasLocation && !useCurrentLocation && ( -

- Share your location first, or enter coordinates manually -

+ {/* Selected location display */} + {selectedLocation && locationMode !== 'manual' && ( +
+
{selectedLocation.name || 'Selected location'}
+
+ {selectedLocation.lat.toFixed(6)}, {selectedLocation.lng.toFixed(6)} +
+
+ )} + + {/* Error display */} + {locationError && ( +

{locationError}

)}
@@ -173,7 +334,7 @@ export default function MeetingPointModal({