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

@@ -1,5 +1,11 @@
import React from 'react';
import { Address } from '../types';
import React, { useState, useEffect, useCallback } from "react";
import { Address } from "../types";
import {
geocodingService,
AddressComponents,
} from "../services/geocodingService";
import AddressAutocomplete from "./AddressAutocomplete";
import { PlaceDetails } from "../services/placesService";
interface LocationFormData {
address1: string;
@@ -17,11 +23,129 @@ interface LocationFormProps {
userAddresses: Address[];
selectedAddressId: string;
addressesLoading: boolean;
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => void;
onChange: (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => void;
onAddressSelect: (addressId: string) => void;
formatAddressDisplay: (address: Address) => string;
onCoordinatesChange?: (latitude: number, longitude: number) => void;
onGeocodeRef?: (geocodeFunction: () => Promise<boolean>) => void;
}
// State constants - moved to top to avoid hoisting issues
const usStates = [
"Alabama",
"Alaska",
"Arizona",
"Arkansas",
"California",
"Colorado",
"Connecticut",
"Delaware",
"Florida",
"Georgia",
"Hawaii",
"Idaho",
"Illinois",
"Indiana",
"Iowa",
"Kansas",
"Kentucky",
"Louisiana",
"Maine",
"Maryland",
"Massachusetts",
"Michigan",
"Minnesota",
"Mississippi",
"Missouri",
"Montana",
"Nebraska",
"Nevada",
"New Hampshire",
"New Jersey",
"New Mexico",
"New York",
"North Carolina",
"North Dakota",
"Ohio",
"Oklahoma",
"Oregon",
"Pennsylvania",
"Rhode Island",
"South Carolina",
"South Dakota",
"Tennessee",
"Texas",
"Utah",
"Vermont",
"Virginia",
"Washington",
"West Virginia",
"Wisconsin",
"Wyoming",
];
// State code to full name mapping for Google Places API integration
const stateCodeToName: { [key: string]: string } = {
AL: "Alabama",
AK: "Alaska",
AZ: "Arizona",
AR: "Arkansas",
CA: "California",
CO: "Colorado",
CT: "Connecticut",
DE: "Delaware",
FL: "Florida",
GA: "Georgia",
HI: "Hawaii",
ID: "Idaho",
IL: "Illinois",
IN: "Indiana",
IA: "Iowa",
KS: "Kansas",
KY: "Kentucky",
LA: "Louisiana",
ME: "Maine",
MD: "Maryland",
MA: "Massachusetts",
MI: "Michigan",
MN: "Minnesota",
MS: "Mississippi",
MO: "Missouri",
MT: "Montana",
NE: "Nebraska",
NV: "Nevada",
NH: "New Hampshire",
NJ: "New Jersey",
NM: "New Mexico",
NY: "New York",
NC: "North Carolina",
ND: "North Dakota",
OH: "Ohio",
OK: "Oklahoma",
OR: "Oregon",
PA: "Pennsylvania",
RI: "Rhode Island",
SC: "South Carolina",
SD: "South Dakota",
TN: "Tennessee",
TX: "Texas",
UT: "Utah",
VT: "Vermont",
VA: "Virginia",
WA: "Washington",
WV: "West Virginia",
WI: "Wisconsin",
WY: "Wyoming",
DC: "District of Columbia",
PR: "Puerto Rico",
VI: "Virgin Islands",
AS: "American Samoa",
GU: "Guam",
MP: "Northern Mariana Islands",
};
const LocationForm: React.FC<LocationFormProps> = ({
data,
userAddresses,
@@ -29,18 +153,155 @@ const LocationForm: React.FC<LocationFormProps> = ({
addressesLoading,
onChange,
onAddressSelect,
formatAddressDisplay
formatAddressDisplay,
onCoordinatesChange,
onGeocodeRef,
}) => {
const usStates = [
"Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut",
"Delaware", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa",
"Kansas", "Kentucky", "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan",
"Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire",
"New Jersey", "New Mexico", "New York", "North Carolina", "North Dakota", "Ohio",
"Oklahoma", "Oregon", "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota",
"Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington", "West Virginia",
"Wisconsin", "Wyoming"
];
const [geocoding, setGeocoding] = useState(false);
const [geocodeError, setGeocodeError] = useState<string | null>(null);
const [geocodeSuccess, setGeocodeSuccess] = useState(false);
const [placesApiError, setPlacesApiError] = useState(false);
// Debounced geocoding function
const geocodeAddress = useCallback(
async (addressData: LocationFormData) => {
if (
!geocodingService.isAddressComplete(addressData as AddressComponents)
) {
return;
}
setGeocoding(true);
setGeocodeError(null);
setGeocodeSuccess(false);
try {
const result = await geocodingService.geocodeAddress(
addressData as AddressComponents
);
if ("error" in result) {
setGeocodeError(result.details || result.error);
} else {
setGeocodeSuccess(true);
if (onCoordinatesChange) {
onCoordinatesChange(result.latitude, result.longitude);
}
// Clear success message after 3 seconds
setTimeout(() => setGeocodeSuccess(false), 3000);
}
} catch (error) {
setGeocodeError("Failed to geocode address");
} finally {
setGeocoding(false);
}
},
[onCoordinatesChange]
);
// Expose geocoding function to parent components
const triggerGeocoding = useCallback(async () => {
if (data.address1 && data.city && data.state && data.zipCode) {
await geocodeAddress(data);
return true; // Successfully triggered
}
return false; // Incomplete address
}, [data, geocodeAddress]);
// Pass geocoding function to parent component
useEffect(() => {
if (onGeocodeRef) {
onGeocodeRef(triggerGeocoding);
}
}, [onGeocodeRef, triggerGeocoding]);
// Handle place selection from autocomplete
const handlePlaceSelect = useCallback(
(place: PlaceDetails) => {
try {
const addressComponents = place.addressComponents;
// Build address1 from street number and route
const streetNumber = addressComponents.streetNumber || "";
const route = addressComponents.route || "";
const address1 = `${streetNumber} ${route}`.trim();
// Create synthetic events to update form data
const createSyntheticEvent = (name: string, value: string) =>
({
target: {
name,
value,
type: "text",
},
} as React.ChangeEvent<HTMLInputElement>);
// Update all address fields
onChange(
createSyntheticEvent("address1", address1 || place.formattedAddress)
);
if (addressComponents.locality) {
onChange(createSyntheticEvent("city", addressComponents.locality));
}
if (addressComponents.administrativeAreaLevel1) {
// Convert state code to full name using mapping, with fallback to long name or original code
const stateCode = addressComponents.administrativeAreaLevel1;
const stateName =
stateCodeToName[stateCode] ||
addressComponents.administrativeAreaLevel1Long ||
stateCode;
// Only set the state if it exists in our dropdown options
if (usStates.includes(stateName)) {
onChange(createSyntheticEvent("state", stateName));
} else {
console.warn(
`State not found in dropdown options: ${stateName} (code: ${stateCode})`
);
}
}
if (addressComponents.postalCode) {
onChange(
createSyntheticEvent("zipCode", addressComponents.postalCode)
);
}
if (addressComponents.country) {
onChange(createSyntheticEvent("country", addressComponents.country));
}
// Set coordinates immediately
if (
onCoordinatesChange &&
place.geometry.latitude &&
place.geometry.longitude
) {
onCoordinatesChange(
place.geometry.latitude,
place.geometry.longitude
);
}
// Clear any previous geocoding messages
setGeocodeError(null);
setGeocodeSuccess(true);
setPlacesApiError(false);
setTimeout(() => setGeocodeSuccess(false), 3000);
} catch (error) {
console.error("Error handling place selection:", error);
setPlacesApiError(true);
}
},
[onChange, onCoordinatesChange]
);
// Handle Places API errors
const handlePlacesApiError = useCallback(() => {
setPlacesApiError(true);
}, []);
return (
<div className="card mb-4">
@@ -48,8 +309,8 @@ const LocationForm: React.FC<LocationFormProps> = ({
<div className="mb-3">
<small className="text-muted">
<i className="bi bi-info-circle me-2"></i>
Your address is private. This will only be used to show
renters a general area.
Your address is private. This will only be used to show renters a
general area.
</small>
</div>
@@ -71,7 +332,7 @@ const LocationForm: React.FC<LocationFormProps> = ({
onChange={(e) => onAddressSelect(e.target.value)}
>
<option value="new">Enter new address</option>
{userAddresses.map(address => (
{userAddresses.map((address) => (
<option key={address.id} value={address.id}>
{formatAddressDisplay(address)}
</option>
@@ -81,24 +342,59 @@ const LocationForm: React.FC<LocationFormProps> = ({
)}
{/* Show form fields for all scenarios with addresses <= 1 or when "new" is selected */}
{(userAddresses.length <= 1 ||
{(userAddresses.length <= 1 ||
(userAddresses.length > 1 && !selectedAddressId)) && (
<>
<div className="row mb-3">
<div className="col-md-6">
<label htmlFor="address1" className="form-label">
Address Line 1 *
</label>
<input
type="text"
className="form-control"
id="address1"
name="address1"
value={data.address1}
onChange={onChange}
placeholder="123 Main Street"
required
/>
<div className="d-flex justify-content-between align-items-center mb-2">
<label htmlFor="address1" className="form-label mb-0">
Address Line 1 *
</label>
</div>
{!placesApiError ? (
<AddressAutocomplete
id="address1"
name="address1"
value={data.address1}
onChange={(value) => {
const syntheticEvent = {
target: {
name: "address1",
value,
type: "text",
},
} as React.ChangeEvent<HTMLInputElement>;
onChange(syntheticEvent);
}}
onPlaceSelect={handlePlaceSelect}
onError={handlePlacesApiError}
placeholder="Start typing an address..."
className="form-control"
required
countryRestriction="us"
types={["address"]}
/>
) : (
<input
type="text"
className="form-control"
id="address1"
name="address1"
value={data.address1}
onChange={onChange}
placeholder="123 Main Street"
required
/>
)}
{placesApiError && (
<div className="text-muted small mt-1">
<i className="bi bi-info-circle me-1"></i>
Address autocomplete is unavailable. Using manual input.
</div>
)}
</div>
<div className="col-md-6">
<label htmlFor="address2" className="form-label">
@@ -144,7 +440,7 @@ const LocationForm: React.FC<LocationFormProps> = ({
required
>
<option value="">Select State</option>
{usStates.map(state => (
{usStates.map((state) => (
<option key={state} value={state}>
{state}
</option>
@@ -176,4 +472,4 @@ const LocationForm: React.FC<LocationFormProps> = ({
);
};
export default LocationForm;
export default LocationForm;