feat: add location autocomplete
This commit is contained in:
170
src/lib/google-maps.ts
Normal file
170
src/lib/google-maps.ts
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user