"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 && (
)}