"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 ( <> {shareToken ? "Sharing location in real-time" : "You are here"} {isConnected && shareToken && (
Connected
)}
{/* Draw path if we have position history and we're on client */} {showPolyline && ( )} ); } // Component to display a shared location function SharedLocationMarker({ position, lastUpdate }: { position: [number, number]; lastUpdate?: string }) { return (
Shared Location
{lastUpdate && (
Last updated: {new Date(lastUpdate).toLocaleTimeString()}
)}
); } 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(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
Loading map...
; } // 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 (
{/* Show our location marker in tracking mode */} {mode === 'tracking' && position && ( )} {/* Show shared location marker in viewing mode */} {mode === 'viewing' && sharedPosition && ( )}
); }