Google maps integration
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user