Google maps integration
This commit is contained in:
@@ -119,9 +119,24 @@ export const stripeAPI = {
|
||||
createAccountLink: (data: { refreshUrl: string; returnUrl: string }) =>
|
||||
api.post("/stripe/account-links", data),
|
||||
getAccountStatus: () => api.get("/stripe/account-status"),
|
||||
createSetupCheckoutSession: (data: {
|
||||
rentalData?: any;
|
||||
}) => api.post("/stripe/create-setup-checkout-session", data),
|
||||
createSetupCheckoutSession: (data: { rentalData?: any }) =>
|
||||
api.post("/stripe/create-setup-checkout-session", data),
|
||||
};
|
||||
|
||||
export const mapsAPI = {
|
||||
placesAutocomplete: (data: {
|
||||
input: string;
|
||||
types?: string[];
|
||||
componentRestrictions?: { country: string };
|
||||
sessionToken?: string;
|
||||
}) => api.post("/maps/places/autocomplete", data),
|
||||
placeDetails: (data: { placeId: string; sessionToken?: string }) =>
|
||||
api.post("/maps/places/details", data),
|
||||
geocode: (data: {
|
||||
address: string;
|
||||
componentRestrictions?: { country: string };
|
||||
}) => api.post("/maps/geocode", data),
|
||||
getHealth: () => api.get("/maps/health"),
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
118
frontend/src/services/geocodingService.ts
Normal file
118
frontend/src/services/geocodingService.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { mapsAPI } from "./api";
|
||||
|
||||
interface AddressComponents {
|
||||
address1: string;
|
||||
address2?: string;
|
||||
city: string;
|
||||
state: string;
|
||||
zipCode: string;
|
||||
country: string;
|
||||
}
|
||||
|
||||
interface GeocodeResult {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
formattedAddress?: string;
|
||||
}
|
||||
|
||||
interface GeocodeError {
|
||||
error: string;
|
||||
details?: string;
|
||||
}
|
||||
|
||||
type GeocodeResponse = GeocodeResult | GeocodeError;
|
||||
|
||||
class GeocodingService {
|
||||
private cache: Map<string, GeocodeResult> = new Map();
|
||||
|
||||
/**
|
||||
* Convert address components to lat/lng coordinates using backend geocoding proxy
|
||||
*/
|
||||
async geocodeAddress(address: AddressComponents): Promise<GeocodeResponse> {
|
||||
// Create address string for geocoding
|
||||
const addressParts = [
|
||||
address.address1,
|
||||
address.address2,
|
||||
address.city,
|
||||
address.state,
|
||||
address.zipCode,
|
||||
address.country,
|
||||
].filter((part) => part && part.trim());
|
||||
|
||||
const addressString = addressParts.join(", ");
|
||||
const cacheKey = addressString.toLowerCase();
|
||||
|
||||
// Check cache first
|
||||
if (this.cache.has(cacheKey)) {
|
||||
return this.cache.get(cacheKey)!;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await mapsAPI.geocode({
|
||||
address: addressString,
|
||||
componentRestrictions: {
|
||||
country: address.country?.toLowerCase() || "us",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.data.latitude && response.data.longitude) {
|
||||
const geocodeResult: GeocodeResult = {
|
||||
latitude: response.data.latitude,
|
||||
longitude: response.data.longitude,
|
||||
formattedAddress: response.data.formattedAddress || addressString,
|
||||
};
|
||||
|
||||
// Cache successful result
|
||||
this.cache.set(cacheKey, geocodeResult);
|
||||
return geocodeResult;
|
||||
} else if (response.data.error) {
|
||||
return {
|
||||
error: "Geocoding failed",
|
||||
details: response.data.error,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
error: "Geocoding failed",
|
||||
details: "No coordinates returned",
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Geocoding API error:", error.message);
|
||||
|
||||
if (error.response?.status === 429) {
|
||||
return {
|
||||
error: "Too many geocoding requests",
|
||||
details: "Please slow down and try again",
|
||||
};
|
||||
} else if (error.response?.status === 401) {
|
||||
return {
|
||||
error: "Authentication required",
|
||||
details: "Please log in to use geocoding",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
error: "Network error during geocoding",
|
||||
details: error.message || "Unknown error",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if address has sufficient components for geocoding
|
||||
*/
|
||||
isAddressComplete(address: AddressComponents): boolean {
|
||||
return !!(
|
||||
address.address1?.trim() &&
|
||||
address.city?.trim() &&
|
||||
address.state?.trim() &&
|
||||
address.zipCode?.trim()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export a singleton instance
|
||||
export const geocodingService = new GeocodingService();
|
||||
|
||||
// Export types for use in other components
|
||||
export type { AddressComponents, GeocodeResult, GeocodeError, GeocodeResponse };
|
||||
135
frontend/src/services/placesService.ts
Normal file
135
frontend/src/services/placesService.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { mapsAPI } from "./api";
|
||||
|
||||
// Define types for place details
|
||||
export interface PlaceDetails {
|
||||
formattedAddress: string;
|
||||
addressComponents: {
|
||||
streetNumber?: string;
|
||||
route?: string;
|
||||
locality?: string;
|
||||
administrativeAreaLevel1?: string;
|
||||
administrativeAreaLevel1Long?: string;
|
||||
postalCode?: string;
|
||||
country?: string;
|
||||
};
|
||||
geometry: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
};
|
||||
placeId: string;
|
||||
}
|
||||
|
||||
export interface AutocompletePrediction {
|
||||
placeId: string;
|
||||
description: string;
|
||||
types: string[];
|
||||
mainText: string;
|
||||
secondaryText: string;
|
||||
}
|
||||
|
||||
class PlacesService {
|
||||
private sessionToken: string | null = null;
|
||||
|
||||
/**
|
||||
* Generate a new session token for cost optimization
|
||||
*/
|
||||
private generateSessionToken(): string {
|
||||
return (
|
||||
Math.random().toString(36).substring(2, 15) +
|
||||
Math.random().toString(36).substring(2, 15)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get autocomplete predictions for a query
|
||||
*/
|
||||
async getAutocompletePredictions(
|
||||
input: string,
|
||||
options?: {
|
||||
types?: string[];
|
||||
componentRestrictions?: { country: string };
|
||||
bounds?: any;
|
||||
}
|
||||
): Promise<AutocompletePrediction[]> {
|
||||
if (input.trim().length < 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Generate new session token if not exists
|
||||
if (!this.sessionToken) {
|
||||
this.sessionToken = this.generateSessionToken();
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await mapsAPI.placesAutocomplete({
|
||||
input: input.trim(),
|
||||
types: options?.types || ["address"],
|
||||
componentRestrictions: options?.componentRestrictions,
|
||||
sessionToken: this.sessionToken || undefined,
|
||||
});
|
||||
|
||||
if (response.data.predictions) {
|
||||
return response.data.predictions;
|
||||
} else if (response.data.error) {
|
||||
console.error("Places Autocomplete API error:", response.data.error);
|
||||
throw new Error(response.data.error);
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching place predictions:", error.message);
|
||||
if (error.response?.status === 429) {
|
||||
throw new Error("Too many requests. Please slow down.");
|
||||
} else if (error.response?.status === 401) {
|
||||
throw new Error("Authentication required. Please log in.");
|
||||
} else {
|
||||
throw new Error("Failed to fetch place suggestions");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get detailed place information by place ID
|
||||
*/
|
||||
async getPlaceDetails(placeId: string): Promise<PlaceDetails> {
|
||||
if (!placeId) {
|
||||
throw new Error("Place ID is required");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await mapsAPI.placeDetails({
|
||||
placeId,
|
||||
sessionToken: this.sessionToken || undefined,
|
||||
});
|
||||
|
||||
// Clear session token after successful place details request
|
||||
this.sessionToken = null;
|
||||
|
||||
if (response.data.placeId) {
|
||||
return {
|
||||
formattedAddress: response.data.formattedAddress,
|
||||
addressComponents: response.data.addressComponents,
|
||||
geometry: response.data.geometry,
|
||||
placeId: response.data.placeId,
|
||||
};
|
||||
} else if (response.data.error) {
|
||||
console.error("Place Details API error:", response.data.error);
|
||||
throw new Error(response.data.error);
|
||||
}
|
||||
|
||||
throw new Error("Invalid response from place details API");
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching place details:", error.message);
|
||||
if (error.response?.status === 429) {
|
||||
throw new Error("Too many requests. Please slow down.");
|
||||
} else if (error.response?.status === 401) {
|
||||
throw new Error("Authentication required. Please log in.");
|
||||
} else {
|
||||
throw new Error("Failed to fetch place details");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export singleton instance
|
||||
export const placesService = new PlacesService();
|
||||
Reference in New Issue
Block a user