Google maps integration

This commit is contained in:
jackiettran
2025-09-09 22:49:55 -04:00
parent 69bf64fe70
commit 1d7db138df
25 changed files with 3711 additions and 577 deletions

View 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;