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

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

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

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