feat: add location autocomplete

This commit is contained in:
2026-04-10 15:40:38 -04:00
parent 12849b2362
commit 27492ee01f
6 changed files with 679 additions and 47 deletions

170
src/lib/google-maps.ts Normal file
View File

@@ -0,0 +1,170 @@
export interface GoogleMapsLocationConfig {
enabled?: boolean | null;
region?: string | null;
publicEnabled?: boolean | null;
serverApiKey?: string | null;
}
export interface GoogleMapsLocationCapability {
enabled: boolean;
reason: "configured" | "missing_server_api_key" | "disabled";
region: string;
}
export interface GoogleMapsPlaceLabelSource {
displayName?: string | { text?: string | null } | null;
formattedAddress?: string | null;
name?: string | null;
predictionText?: string | null;
}
export interface GoogleMapsSuggestion {
formattedAddress?: string;
placeId?: string;
text: string;
}
const DEFAULT_GOOGLE_MAPS_REGION = "us";
const normalizeConfigValue = (value?: string | null) => {
const trimmedValue = value?.trim();
return trimmedValue ? trimmedValue : null;
};
const getDisplayNameText = (
displayName?: string | { text?: string | null } | null,
) => {
if (typeof displayName === "string") {
return normalizeConfigValue(displayName);
}
return normalizeConfigValue(displayName?.text);
};
const parseEnabledValue = (value?: string | null) => {
const normalizedValue = normalizeConfigValue(value)?.toLowerCase();
if (!normalizedValue) {
return true;
}
return !["0", "false", "no", "off"].includes(normalizedValue);
};
export const getGoogleMapsLocationCapability = (
config: GoogleMapsLocationConfig = {},
): GoogleMapsLocationCapability => {
const enabled =
typeof config.enabled === "boolean"
? config.enabled
: typeof config.publicEnabled === "boolean"
? config.publicEnabled
: parseEnabledValue(
process.env.NEXT_PUBLIC_GOOGLE_MAPS_AUTOCOMPLETE_ENABLED,
);
const serverApiKey = normalizeConfigValue(
config.serverApiKey ?? process.env.GOOGLE_MAPS_API_KEY,
);
const region =
normalizeConfigValue(config.region ?? process.env.GOOGLE_MAPS_REGION) ??
DEFAULT_GOOGLE_MAPS_REGION;
if (!enabled) {
return {
enabled: false,
reason: "disabled",
region,
};
}
if (typeof window !== "undefined") {
return {
enabled: true,
reason: "configured",
region,
};
}
if (!serverApiKey) {
return {
enabled: false,
reason: "missing_server_api_key",
region,
};
}
return {
enabled: true,
reason: "configured",
region,
};
};
export const getGoogleMapsServerApiKey = () =>
normalizeConfigValue(process.env.GOOGLE_MAPS_API_KEY);
export const getGoogleMapsPlaceLabel = (source: GoogleMapsPlaceLabelSource) => {
return (
normalizeConfigValue(source.predictionText) ??
getDisplayNameText(source.displayName) ??
normalizeConfigValue(source.formattedAddress) ??
normalizeConfigValue(source.name) ??
""
);
};
export const getLocationAutocompletePlaceholder = (
capability: GoogleMapsLocationCapability,
) => {
return capability.enabled
? "Search Google Maps or type a location"
: "Location";
};
interface GooglePlacesAutocompleteResponse {
suggestions?: Array<{
placePrediction?: {
place?: string;
placeId?: string;
text?: { text?: string | null };
structuredFormat?: {
mainText?: { text?: string | null };
secondaryText?: { text?: string | null };
};
};
}>;
}
export const mapGooglePlacesSuggestions = (
payload: GooglePlacesAutocompleteResponse,
): GoogleMapsSuggestion[] => {
const suggestions: GoogleMapsSuggestion[] = [];
for (const suggestion of payload.suggestions ?? []) {
const placePrediction = suggestion.placePrediction;
const text =
normalizeConfigValue(placePrediction?.text?.text) ??
normalizeConfigValue(placePrediction?.structuredFormat?.mainText?.text) ??
"";
if (!text) {
continue;
}
suggestions.push({
formattedAddress:
normalizeConfigValue(
placePrediction?.structuredFormat?.secondaryText?.text,
) ?? undefined,
placeId:
normalizeConfigValue(placePrediction?.placeId) ??
normalizeConfigValue(
placePrediction?.place?.replace(/^places\//, ""),
) ??
undefined,
text,
});
}
return suggestions;
};