diff options
Diffstat (limited to 'app/src/components/Map.tsx')
-rw-r--r-- | app/src/components/Map.tsx | 273 |
1 files changed, 273 insertions, 0 deletions
diff --git a/app/src/components/Map.tsx b/app/src/components/Map.tsx new file mode 100644 index 0000000..c6e9583 --- /dev/null +++ b/app/src/components/Map.tsx @@ -0,0 +1,273 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { MapContainer, TileLayer, Marker, Popup, useMap, Polyline } from "react-leaflet"; +import L from "leaflet"; +import "leaflet/dist/leaflet.css"; +import { toast } from "sonner"; +import { useSocket } from "@/hooks/useSocket"; + +// Type for location +interface LocationType { + latitude: number; + longitude: number; + accuracy?: number; + timestamp: Date; +} + +// Location update handler that recalculates position +function LocationMarker({ + position, + onLocationUpdate, + shareToken, +}: { + position: [number, number] | null; + onLocationUpdate: (location: LocationType) => void; + shareToken?: string; +}) { + const [positionHistory, setPositionHistory] = useState<[number, number][]>([]); + const [isClient, setIsClient] = useState(false); + const map = useMap(); + const { sendLocationUpdate, isConnected } = useSocket(); + + // Safely check if we're on the client side + useEffect(() => { + setIsClient(true); + }, []); + + useEffect(() => { + map.locate({ watch: true, enableHighAccuracy: true }); + + map.on("locationfound", (e) => { + const newPosition: [number, number] = [e.latlng.lat, e.latlng.lng]; + + // Update position history + setPositionHistory((prev) => [...prev, newPosition]); + + // Create location data + const locationData = { + latitude: e.latlng.lat, + longitude: e.latlng.lng, + accuracy: e.accuracy, + timestamp: new Date(), + }; + + // Notify parent component + onLocationUpdate(locationData); + + // Send location update to Socket.IO if sharing + if (shareToken && isConnected) { + sendLocationUpdate({ + latitude: e.latlng.lat, + longitude: e.latlng.lng, + accuracy: e.accuracy, + shareToken, + }); + } + + // Center map on the new position + map.flyTo(e.latlng, map.getZoom()); + }); + + map.on("locationerror", (e) => { + toast.error("Error accessing location: " + e.message); + console.error("Location error: ", e); + }); + + return () => { + map.stopLocate(); + map.off("locationfound"); + map.off("locationerror"); + }; + }, [map, onLocationUpdate, sendLocationUpdate, shareToken, isConnected]); + + // Return null if position is null or if we're not on client yet + if (!position || !isClient) return null; + + // Only render the polyline if we have enough points and we're on the client side + const showPolyline = isClient && positionHistory && positionHistory.length > 1; + + return ( + <> + <Marker position={position}> + <Popup> + {shareToken ? "Sharing location in real-time" : "You are here"} + {isConnected && shareToken && ( + <div className="text-xs mt-1 text-green-600">Connected</div> + )} + </Popup> + </Marker> + + {/* Draw path if we have position history and we're on client */} + {showPolyline && ( + <Polyline + pathOptions={{ color: "blue", weight: 3 }} + positions={positionHistory} + /> + )} + </> + ); +} + +// Component to display a shared location +function SharedLocationMarker({ position, lastUpdate }: { position: [number, number]; lastUpdate?: string }) { + return ( + <Marker position={position} icon={new L.Icon({ + iconUrl: '/marker-icon-red.png', + iconRetinaUrl: '/marker-icon-2x-red.png', + shadowUrl: '/marker-shadow.png', + iconSize: [25, 41], + iconAnchor: [12, 41], + popupAnchor: [1, -34], + shadowSize: [41, 41] + })}> + <Popup> + <div> + <div>Shared Location</div> + {lastUpdate && ( + <div className="text-xs mt-1">Last updated: {new Date(lastUpdate).toLocaleTimeString()}</div> + )} + </div> + </Popup> + </Marker> + ); +} + +export default function Map({ + onLocationUpdate, + shareToken, + mode = 'tracking', + initialLocation, +}: { + onLocationUpdate?: (location: LocationType) => void; + shareToken?: string; + mode?: 'tracking' | 'viewing'; + initialLocation?: { latitude: number; longitude: number }; +}) { + const [position, setPosition] = useState<[number, number] | null>(null); + const [sharedPosition, setSharedPosition] = useState<[number, number] | null>(null); + const [lastUpdate, setLastUpdate] = useState<string | undefined>(undefined); + const [isLoading, setIsLoading] = useState(true); + const [isClient, setIsClient] = useState(false); + const { subscribeToLocationUpdates } = useSocket(); + + // Check if we're on client side + useEffect(() => { + setIsClient(true); + }, []); + + // Fix Leaflet marker icon issues in Next.js + useEffect(() => { + // This is needed to fix the marker icon issues with webpack + if (typeof window !== "undefined") { + // @ts-ignore + delete L.Icon.Default.prototype._getIconUrl; + L.Icon.Default.mergeOptions({ + iconRetinaUrl: "/marker-icon-2x.png", + iconUrl: "/marker-icon.png", + shadowUrl: "/marker-shadow.png", + }); + } + }, []); + + // Subscribe to real-time location updates when viewing shared location + useEffect(() => { + if (mode === 'viewing' && shareToken && isClient) { + // If we have initial location, set it + if (initialLocation) { + setSharedPosition([initialLocation.latitude, initialLocation.longitude]); + setIsLoading(false); + } + + // Subscribe to real-time updates + const cleanup = subscribeToLocationUpdates(shareToken, (data) => { + console.log('Received location update:', data); + setSharedPosition([data.latitude, data.longitude]); + setLastUpdate(data.timestamp); + toast.info('Location updated'); + }); + + return cleanup; + } + }, [mode, shareToken, subscribeToLocationUpdates, initialLocation, isClient]); + + useEffect(() => { + if (!isClient) return; + + // Only get current position in tracking mode + if (mode === 'tracking') { + // Try to get initial position + navigator.geolocation.getCurrentPosition( + (position) => { + setPosition([position.coords.latitude, position.coords.longitude]); + setIsLoading(false); + + if (onLocationUpdate) { + onLocationUpdate({ + latitude: position.coords.latitude, + longitude: position.coords.longitude, + accuracy: position.coords.accuracy, + timestamp: new Date(position.timestamp), + }); + } + }, + (error) => { + toast.error(`Error getting location: ${error.message}`); + setIsLoading(false); + }, + { enableHighAccuracy: true } + ); + } + }, [onLocationUpdate, isClient, mode]); + + // Handle location updates from the marker component + const handleLocationUpdate = (location: LocationType) => { + setPosition([location.latitude, location.longitude]); + if (onLocationUpdate) { + onLocationUpdate(location); + } + }; + + if (!isClient || isLoading) { + return <div className="h-full w-full flex items-center justify-center">Loading map...</div>; + } + + // Determine which position to center on + let defaultPosition: [number, number]; + if (mode === 'viewing' && sharedPosition) { + defaultPosition = sharedPosition; + } else if (position) { + defaultPosition = position; + } else { + defaultPosition = [51.505, -0.09]; // Default to London + } + + return ( + <div className="h-full w-full"> + <MapContainer + center={defaultPosition} + zoom={13} + style={{ height: "100%", width: "100%" }} + > + <TileLayer + attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' + url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" + /> + + {/* Show our location marker in tracking mode */} + {mode === 'tracking' && position && ( + <LocationMarker + position={position} + onLocationUpdate={handleLocationUpdate} + shareToken={shareToken} + /> + )} + + {/* Show shared location marker in viewing mode */} + {mode === 'viewing' && sharedPosition && ( + <SharedLocationMarker position={sharedPosition} lastUpdate={lastUpdate} /> + )} + </MapContainer> + </div> + ); +}
\ No newline at end of file |