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,42 +1,53 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { placesService, AutocompletePrediction, PlaceDetails } from '../services/placesService';
interface AddressSuggestion {
place_id: string;
display_name: string;
lat: string;
lon: string;
}
interface AddressAutocompleteProps {
export interface AddressAutocompleteProps {
value: string;
onChange: (value: string, lat?: number, lon?: number) => void;
onChange: (value: string) => void;
onPlaceSelect?: (place: PlaceDetails) => void;
onError?: (error: string) => void;
placeholder?: string;
required?: boolean;
className?: string;
id?: string;
name?: string;
required?: boolean;
countryRestriction?: string;
types?: string[];
disabled?: boolean;
}
const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
value,
onChange,
placeholder = "Address",
required = false,
onPlaceSelect,
onError,
placeholder = "Enter address",
className = "form-control",
id,
name
name,
required = false,
countryRestriction,
types = ['address'],
disabled = false
}) => {
const [suggestions, setSuggestions] = useState<AddressSuggestion[]>([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const [predictions, setPredictions] = useState<AutocompletePrediction[]>([]);
const [showDropdown, setShowDropdown] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedIndex, setSelectedIndex] = useState(-1);
const [isSelectingPlace, setIsSelectingPlace] = useState(false);
const wrapperRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const debounceTimer = useRef<number | undefined>(undefined);
// Handle clicking outside to close suggestions
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) {
setShowSuggestions(false);
setShowDropdown(false);
setSelectedIndex(-1);
}
};
@@ -55,100 +66,249 @@ const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
};
}, []);
const fetchAddressSuggestions = async (query: string) => {
if (query.length < 3) {
setSuggestions([]);
// Fetch autocomplete predictions
const fetchPredictions = useCallback(async (query: string) => {
if (query.trim().length < 2 || isSelectingPlace) {
setPredictions([]);
return;
}
setLoading(true);
setError(null);
try {
// Using Nominatim API (OpenStreetMap) for free geocoding
// In production, you might want to use Google Places API or another service
const response = await fetch(
`https://nominatim.openstreetmap.org/search?` +
`q=${encodeURIComponent(query)}&` +
`format=json&` +
`limit=5&` +
`countrycodes=us`
);
const options = {
types,
componentRestrictions: countryRestriction ? { country: countryRestriction } : undefined
};
const results = await placesService.getAutocompletePredictions(query, options);
setPredictions(results);
} catch (err) {
console.error('Error fetching place predictions:', err);
const errorMessage = 'Failed to load suggestions';
setError(errorMessage);
setPredictions([]);
if (response.ok) {
const data = await response.json();
setSuggestions(data);
// Notify parent component of error
if (onError) {
onError(errorMessage);
}
} catch (error) {
console.error('Error fetching address suggestions:', error);
setSuggestions([]);
} finally {
setLoading(false);
}
};
}, [types, countryRestriction, isSelectingPlace, onError]);
// Handle input change with debouncing
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
onChange(newValue);
setShowSuggestions(true);
setShowDropdown(true);
setSelectedIndex(-1);
setIsSelectingPlace(false);
// Debounce the API call
// Clear previous timer
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
// Debounce API calls
debounceTimer.current = window.setTimeout(() => {
fetchAddressSuggestions(newValue);
fetchPredictions(newValue);
}, 300);
};
const handleSuggestionClick = (suggestion: AddressSuggestion) => {
onChange(
suggestion.display_name,
parseFloat(suggestion.lat),
parseFloat(suggestion.lon)
);
setShowSuggestions(false);
setSuggestions([]);
// Handle place selection
const handlePlaceSelect = async (prediction: AutocompletePrediction) => {
setIsSelectingPlace(true);
setLoading(true);
setShowDropdown(false);
setSelectedIndex(-1);
try {
const placeDetails = await placesService.getPlaceDetails(prediction.placeId);
onChange(placeDetails.formattedAddress);
if (onPlaceSelect) {
onPlaceSelect(placeDetails);
}
} catch (err) {
console.error('Error fetching place details:', err);
const errorMessage = 'Failed to load place details';
setError(errorMessage);
// Keep the description as fallback
onChange(prediction.description);
// Notify parent component of error
if (onError) {
onError(errorMessage);
}
} finally {
setLoading(false);
setIsSelectingPlace(false);
}
};
// Handle keyboard navigation
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (!showDropdown || predictions.length === 0) {
return;
}
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setSelectedIndex(prev =>
prev < predictions.length - 1 ? prev + 1 : prev
);
break;
case 'ArrowUp':
e.preventDefault();
setSelectedIndex(prev => prev > 0 ? prev - 1 : -1);
break;
case 'Enter':
e.preventDefault();
if (selectedIndex >= 0 && selectedIndex < predictions.length) {
handlePlaceSelect(predictions[selectedIndex]);
}
break;
case 'Escape':
setShowDropdown(false);
setSelectedIndex(-1);
inputRef.current?.blur();
break;
default:
break;
}
};
// Handle input focus
const handleFocus = () => {
if (predictions.length > 0) {
setShowDropdown(true);
}
};
return (
<div ref={wrapperRef} className="position-relative">
<input
ref={inputRef}
type="text"
className={className}
className={`${className} ${loading ? 'pe-5' : ''}`}
id={id}
name={name}
value={value}
onChange={handleInputChange}
onFocus={() => setShowSuggestions(true)}
onKeyDown={handleKeyDown}
onFocus={handleFocus}
placeholder={placeholder}
required={required}
disabled={disabled}
autoComplete="off"
aria-expanded={showDropdown}
aria-haspopup="listbox"
aria-owns={showDropdown ? `${id}-dropdown` : undefined}
aria-activedescendant={
selectedIndex >= 0 ? `${id}-option-${selectedIndex}` : undefined
}
/>
{showSuggestions && (suggestions.length > 0 || loading) && (
{/* Loading spinner */}
{loading && (
<div
className="position-absolute w-100 bg-white border rounded-bottom shadow-sm"
style={{ top: '100%', zIndex: 1000, maxHeight: '300px', overflowY: 'auto' }}
className="position-absolute top-50 end-0 translate-middle-y me-3"
style={{ pointerEvents: 'none' }}
>
{loading ? (
<div className="p-2 text-center text-muted">
<small>Searching addresses...</small>
<div
className="spinner-border spinner-border-sm text-muted"
role="status"
style={{ width: '1rem', height: '1rem' }}
>
<span className="visually-hidden">Loading...</span>
</div>
</div>
)}
{/* Dropdown with predictions */}
{showDropdown && (predictions.length > 0 || error || loading) && (
<div
ref={dropdownRef}
id={`${id}-dropdown`}
className="position-absolute w-100 bg-white border rounded-bottom shadow-sm"
style={{
top: '100%',
zIndex: 1050,
maxHeight: '300px',
overflowY: 'auto',
borderTop: 'none',
borderTopLeftRadius: 0,
borderTopRightRadius: 0
}}
role="listbox"
>
{error && (
<div className="p-3 text-center text-danger">
<small>
<i className="bi bi-exclamation-triangle me-1"></i>
{error}
</small>
</div>
) : (
suggestions.map((suggestion) => (
<div
key={suggestion.place_id}
className="p-2 border-bottom cursor-pointer"
style={{ cursor: 'pointer' }}
onClick={() => handleSuggestionClick(suggestion)}
onMouseEnter={(e) => e.currentTarget.classList.add('bg-light')}
onMouseLeave={(e) => e.currentTarget.classList.remove('bg-light')}
>
<small className="d-block text-truncate">
{suggestion.display_name}
</small>
)}
{loading && predictions.length === 0 && !error && (
<div className="p-3 text-center text-muted">
<small>
<i className="bi bi-search me-1"></i>
Searching addresses...
</small>
</div>
)}
{predictions.map((prediction, index) => (
<div
key={prediction.placeId}
id={`${id}-option-${index}`}
className={`p-3 border-bottom cursor-pointer ${
index === selectedIndex ? 'bg-light' : ''
}`}
style={{ cursor: 'pointer' }}
onClick={() => handlePlaceSelect(prediction)}
onMouseEnter={() => setSelectedIndex(index)}
onMouseLeave={() => setSelectedIndex(-1)}
role="option"
aria-selected={index === selectedIndex}
>
<div className="d-flex align-items-start">
<i className="bi bi-geo-alt text-muted me-2 mt-1" style={{ fontSize: '0.875rem' }}></i>
<div className="flex-grow-1 min-width-0">
<div className="text-truncate fw-medium">
{prediction.mainText}
</div>
{prediction.secondaryText && (
<div className="text-muted small text-truncate">
{prediction.secondaryText}
</div>
)}
</div>
</div>
))
</div>
))}
{predictions.length > 0 && (
<div className="p-2 text-center border-top bg-light">
<small className="text-muted d-flex align-items-center justify-content-center">
<img
src="https://developers.google.com/maps/documentation/places/web-service/images/powered_by_google_on_white.png"
alt="Powered by Google"
style={{ height: '12px' }}
/>
</small>
</div>
)}
</div>
)}

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;

View File

@@ -29,7 +29,7 @@ const ItemCard: React.FC<ItemCardProps> = ({
const getLocationDisplay = () => {
return item.city && item.state
? `${item.city}, ${item.state}`
: item.location;
: '';
};
return (

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;