Google maps integration
This commit is contained in:
151
frontend/src/components/GoogleMapWithRadius.tsx
Normal file
151
frontend/src/components/GoogleMapWithRadius.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import React, { useMemo, useEffect, useRef } from "react";
|
||||
import { Loader } from "@googlemaps/js-api-loader";
|
||||
|
||||
interface GoogleMapWithRadiusProps {
|
||||
latitude?: number | string;
|
||||
longitude?: number | string;
|
||||
mapOptions?: {
|
||||
zoom?: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Utility function to safely convert coordinates to numbers
|
||||
const safeParseNumber = (value: number | string | undefined): number | null => {
|
||||
if (value === undefined || value === null || value === "") return null;
|
||||
const num = typeof value === "string" ? parseFloat(value) : value;
|
||||
return !isNaN(num) && isFinite(num) ? num : null;
|
||||
};
|
||||
|
||||
// 2 miles in meters for radius circle
|
||||
const RADIUS_METERS = 2 * 1609.34;
|
||||
|
||||
const GoogleMapWithRadius: React.FC<GoogleMapWithRadiusProps> = ({
|
||||
latitude: rawLatitude,
|
||||
longitude: rawLongitude,
|
||||
mapOptions = {},
|
||||
}) => {
|
||||
// Convert coordinates to numbers safely
|
||||
const latitude = safeParseNumber(rawLatitude);
|
||||
const longitude = safeParseNumber(rawLongitude);
|
||||
|
||||
// Destructure mapOptions to create stable references
|
||||
const { zoom = 12 } = mapOptions;
|
||||
|
||||
// Get API key from environment
|
||||
const apiKey = process.env.REACT_APP_GOOGLE_MAPS_PUBLIC_API_KEY;
|
||||
|
||||
// Refs for map container and instances
|
||||
const mapRef = useRef<HTMLDivElement>(null);
|
||||
const mapInstanceRef = useRef<google.maps.Map | null>(null);
|
||||
const circleRef = useRef<google.maps.Circle | null>(null);
|
||||
|
||||
// Memoize map center
|
||||
const mapCenter = useMemo(() => {
|
||||
if (latitude === null || longitude === null) return null;
|
||||
return { lat: latitude, lng: longitude };
|
||||
}, [latitude, longitude]);
|
||||
|
||||
// Initialize map
|
||||
useEffect(() => {
|
||||
if (!apiKey || !mapRef.current || !mapCenter) return;
|
||||
|
||||
const initializeMap = async () => {
|
||||
const loader = new Loader({
|
||||
apiKey,
|
||||
version: "weekly",
|
||||
});
|
||||
|
||||
try {
|
||||
await loader.importLibrary("maps");
|
||||
|
||||
if (!mapRef.current) return;
|
||||
|
||||
// Create map
|
||||
const map = new google.maps.Map(mapRef.current, {
|
||||
center: mapCenter,
|
||||
zoom: zoom,
|
||||
zoomControl: true,
|
||||
mapTypeControl: false,
|
||||
scaleControl: true,
|
||||
streetViewControl: false,
|
||||
rotateControl: false,
|
||||
fullscreenControl: false,
|
||||
});
|
||||
|
||||
mapInstanceRef.current = map;
|
||||
|
||||
// Create circle overlay
|
||||
const circle = new google.maps.Circle({
|
||||
center: mapCenter,
|
||||
radius: RADIUS_METERS,
|
||||
fillColor: "#6c757d",
|
||||
fillOpacity: 0.2,
|
||||
strokeColor: "#6c757d",
|
||||
strokeOpacity: 0.8,
|
||||
strokeWeight: 2,
|
||||
map: map,
|
||||
});
|
||||
|
||||
circleRef.current = circle;
|
||||
} catch (error) {
|
||||
console.error("Failed to load Google Maps:", error);
|
||||
}
|
||||
};
|
||||
|
||||
initializeMap();
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
if (circleRef.current) {
|
||||
circleRef.current.setMap(null);
|
||||
}
|
||||
mapInstanceRef.current = null;
|
||||
};
|
||||
}, [apiKey, mapCenter, zoom]);
|
||||
|
||||
// Update map center and circle when coordinates change
|
||||
useEffect(() => {
|
||||
if (!mapInstanceRef.current || !circleRef.current || !mapCenter) return;
|
||||
|
||||
mapInstanceRef.current.setCenter(mapCenter);
|
||||
circleRef.current.setCenter(mapCenter);
|
||||
}, [mapCenter]);
|
||||
|
||||
// Handle case where no coordinates are available
|
||||
if (latitude === null || longitude === null || mapCenter === null) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<h5>Location</h5>
|
||||
<div
|
||||
className="d-flex align-items-center justify-content-center"
|
||||
style={{
|
||||
height: "300px",
|
||||
backgroundColor: "#f8f9fa",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
>
|
||||
<div className="text-center">
|
||||
<p className="text-muted small">Map unavailable</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<h5>Location</h5>
|
||||
<div
|
||||
ref={mapRef}
|
||||
style={{
|
||||
height: "300px",
|
||||
borderRadius: "8px",
|
||||
backgroundColor: "#f8f9fa",
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoogleMapWithRadius;
|
||||
Reference in New Issue
Block a user