Skip to content

Commit

Permalink
fix: geocoding on address visiblity change (#1316)
Browse files Browse the repository at this point in the history
<!--- Please provide a general summary of your changes in the title
above -->

# Pull Request type

<!-- Please try to limit your pull request to one type; submit multiple
pull requests if needed. -->

Please check the type of change your PR introduces:

- [x] Bugfix
- [ ] Feature
- [ ] Code style update (formatting, renaming)
- [ ] Refactoring (no functional changes, no API changes)
- [ ] Build-related changes
- [ ] Documentation content changes
- [ ] Other (please describe):

## What is the current behavior?

<!-- Please describe the current behavior that you are modifying, or
link to a relevant issue. -->

Issue Number: N/A

## What is the new behavior?

<!-- Please describe the behavior or changes that are being added by
this PR. -->

-
-
-

## Does this introduce a breaking change?

- [ ] Yes
- [ ] No

<!-- If this does introduce a breaking change, please describe the
impact and migration path for existing applications below. -->

## Other information

<!-- Any other information that is important to this PR, such as
screenshots of how the component looks before and after the change. -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
  - Added support for city coordinate queries via a new API endpoint.
- Introduced React components `AutoCompleteItem` and `CountryItem` for
improved UI interaction.
  - Added a `FormContext` for better form state management in the UI.

- **Enhancements**
- Improved address visibility handling and formatting across multiple
components.
  - Refined Google geocoding and autocomplete functionality.

- **Bug Fixes**
- Corrected naming in API handlers to improve clarity and maintain
consistency.

- **Styling**
- Added new styling rules for elements in the `AddressDrawer` component
for a more polished UI.

- **Refactor**
- Consolidated schema definitions for Google API responses to reduce
redundancy.
- Updated import paths for better module organization and
maintainability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Joe Karow <[email protected]>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Jul 11, 2024
1 parent 89d952e commit cb86839
Show file tree
Hide file tree
Showing 18 changed files with 608 additions and 341 deletions.
4 changes: 4 additions & 0 deletions packages/api/router/geo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@ export const geoRouter = defineRouter({
)
return handler(opts)
}),
cityCoords: publicProcedure.input(schema.ZCityCoordsSchema).query(async (opts) => {
const handler = await importHandler(namespaced('cityCoords'), () => import('./query.cityCoords.handler'))
return handler(opts)
}),
})
58 changes: 58 additions & 0 deletions packages/api/router/geo/query.cityCoords.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { PlaceAutocompleteType } from '@googlemaps/google-maps-services-js'
import { TRPCError } from '@trpc/server'

import { prisma } from '@weareinreach/db'
import { googleMapsApi } from '~api/google'
import { handleError } from '~api/lib/errorHandler'
import { googleAPIResponseHandler } from '~api/lib/googleHandler'
import { geocodeByStringResponse } from '~api/schemas/thirdParty/googleGeo'
import { type TRPCHandlerParams } from '~api/types/handler'

import { type TCityCoordsSchema } from './query.cityCoords.schema'

const countryMap = new Map<string, string>()
const govDistMap = new Map<string, string>()

const cityCoords = async ({ input }: TRPCHandlerParams<TCityCoordsSchema>) => {
try {
const { city, govDist, country } = input
if (countryMap.size === 0) {
const countryData = await prisma.country.findMany({
where: { activeForOrgs: true },
select: { id: true, cca2: true },
})
countryData.forEach((x) => countryMap.set(x.id, x.cca2))
}
if (govDist) {
const govDistData = await prisma.govDist.findMany({
where: { countryId: country, active: true },
select: { id: true, name: true },
})
govDistData.forEach((x) => govDistMap.set(x.id, x.name))
}
const searchCountry = country ? countryMap.get(country) : undefined
if (!searchCountry) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Country not found',
})
}
const searchGovDist = govDist ? govDistMap.get(govDist) : undefined
const searchString = [city, searchGovDist, searchCountry].filter(Boolean).join(', ')
const { data } = await googleMapsApi.geocode({
params: {
// eslint-disable-next-line node/no-process-env
key: process.env.GOOGLE_PLACES_API_KEY as string,
address: searchString,
components: PlaceAutocompleteType.cities,
},
})

const parsedData = geocodeByStringResponse.parse(data)

return googleAPIResponseHandler(parsedData, data)
} catch (error) {
return handleError(error)
}
}
export default cityCoords
10 changes: 10 additions & 0 deletions packages/api/router/geo/query.cityCoords.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { z } from 'zod'

import { prefixedId } from '~api/schemas/idPrefix'

export const ZCityCoordsSchema = z.object({
city: z.string(),
govDist: prefixedId('govDist').nullish(),
country: prefixedId('country'),
})
export type TCityCoordsSchema = z.infer<typeof ZCityCoordsSchema>
4 changes: 2 additions & 2 deletions packages/api/router/geo/query.geoByPlaceId.handler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable node/no-process-env */
import { googleMapsApi } from '~api/google'
import { googleAPIResponseHandler } from '~api/lib/googleHandler'
import { geocodeResponse } from '~api/schemas/thirdParty/googleGeo'
import { geocodeByPlaceIdResponse } from '~api/schemas/thirdParty/googleGeo'
import { type TRPCHandlerParams } from '~api/types/handler'

import { type TGeoByPlaceIdSchema } from './query.geoByPlaceId.schema'
Expand All @@ -13,7 +13,7 @@ const geoByPlaceId = async ({ input }: TRPCHandlerParams<TGeoByPlaceIdSchema>) =
place_id: input,
},
})
const parsedData = geocodeResponse.parse(data)
const parsedData = geocodeByPlaceIdResponse.parse(data)
return googleAPIResponseHandler(parsedData, data)
}
export default geoByPlaceId
1 change: 1 addition & 0 deletions packages/api/router/geo/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// codegen:start {preset: barrel, include: ./*.schema.ts}
export * from './query.autocomplete.schema'
export * from './query.cityCoords.schema'
export * from './query.geoByPlaceId.schema'
// codegen:end
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { prisma, PrismaEnums } from '@weareinreach/db'
import { prisma } from '@weareinreach/db'
import { attributes } from '~api/schemas/selects/common'
import { globalSelect, globalWhere } from '~api/selects/global'
import { type TRPCHandlerParams } from '~api/types/handler'
Expand Down
6 changes: 5 additions & 1 deletion packages/api/router/organization/query.forOrgPage.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { attributes, freeText, isPublic } from '~api/schemas/selects/common'
import { type TRPCHandlerParams } from '~api/types/handler'

import { type TForOrgPageSchema } from './query.forOrgPage.schema'
import { formatAddressVisiblity } from '../location/lib.formatAddressVisibility'

const forOrgPage = async ({ input }: TRPCHandlerParams<TForOrgPageSchema>) => {
try {
Expand Down Expand Up @@ -37,14 +38,17 @@ const forOrgPage = async ({ input }: TRPCHandlerParams<TForOrgPageSchema>) => {
country: { select: { cca2: true } },
govDist: { select: { abbrev: true, tsKey: true, tsNs: true } },
addressVisibility: true,
latitude: true,
longitude: true,
},
},
attributes,
},
})
const { allowedEditors, ...orgData } = org
const { allowedEditors, locations, ...orgData } = org
const reformatted = {
...orgData,
locations: locations.map((location) => ({ ...location, ...formatAddressVisiblity(location) })),
isClaimed: Boolean(allowedEditors.length),
}

Expand Down
236 changes: 126 additions & 110 deletions packages/api/schemas/thirdParty/googleGeo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,133 +2,149 @@ import { GeocodingAddressComponentType, PlaceType2 } from '@googlemaps/google-ma
import compact from 'just-compact'
import { z } from 'zod'

export const autocompleteResponse = z
.object({
predictions: z
.object({
place_id: z.string(),
structured_formatting: z.object({
main_text: z.string(),
secondary_text: z.string(),
}),
})
.array(),
status: z.enum([
'OK',
'ZERO_RESULTS',
'INVALID_REQUEST',
'OVER_QUERY_LIMIT',
'REQUEST_DENIED',
'UNKNOWN_ERROR',
]),
error_message: z.string().optional(),
info_messages: z.string().array().optional(),
})
.transform(({ predictions, ...data }) => ({
...data,
results: predictions.map((result) => ({
value: `${result.structured_formatting.main_text}, ${result.structured_formatting.secondary_text}`,
label: result.structured_formatting.main_text,
subheading: result.structured_formatting.secondary_text,
placeId: result.place_id,
})),
}))
const responseStatus = z.enum([
'OK',
'ZERO_RESULTS',
'INVALID_REQUEST',
'OVER_QUERY_LIMIT',
'REQUEST_DENIED',
'UNKNOWN_ERROR',
])

const GoogleResponse = z.object({
status: responseStatus,
error_message: z.string().optional(),
info_messages: z.string().array().optional(),
})

export const autocompleteResponse = GoogleResponse.extend({
predictions: z
.object({
place_id: z.string(),
structured_formatting: z.object({
main_text: z.string(),
secondary_text: z.string(),
}),
})
.array(),
}).transform(({ predictions, ...data }) => ({
...data,
results: predictions.map((result) => ({
value: `${result.structured_formatting.main_text}, ${result.structured_formatting.secondary_text}`,
label: result.structured_formatting.main_text,
subheading: result.structured_formatting.secondary_text,
placeId: result.place_id,
})),
}))

const coordinates = z.object({
lat: z.number(),
lng: z.number(),
})
const BoundsSchema = z.object({
northeast: coordinates,
southwest: coordinates,
})

type AddressPart = GeocodingAddressComponentType | PlaceType2
export const geocodeResponse = z
.object({
results: z
.object({
geometry: z.object({
location: coordinates,
bounds: z
.object({
northeast: coordinates,
southwest: coordinates,
})
.optional(),
viewport: z.object({
northeast: coordinates,
southwest: coordinates,
}),
}),
address_components: z
.object({
long_name: z.string(),
short_name: z.string(),
types: z.union([z.nativeEnum(GeocodingAddressComponentType), z.nativeEnum(PlaceType2)]).array(),
})
.array(),
})
.array(),
status: z.enum([
'OK',
'ZERO_RESULTS',
'INVALID_REQUEST',
'OVER_QUERY_LIMIT',
'REQUEST_DENIED',
'UNKNOWN_ERROR',
]),
error_message: z.string().optional(),
info_messages: z.string().array().optional(),
})
.transform(({ results, ...data }) => {
const result = results[0]
if (!result) return { result: undefined, ...data }
export const geocodeByPlaceIdResponse = GoogleResponse.extend({
results: z
.object({
geometry: z.object({
location: coordinates,
bounds: BoundsSchema.optional(),
viewport: BoundsSchema,
}),
address_components: z
.object({
long_name: z.string(),
short_name: z.string(),
types: z.union([z.nativeEnum(GeocodingAddressComponentType), z.nativeEnum(PlaceType2)]).array(),
})
.array(),
})
.array(),
}).transform(({ results, ...data }) => {
const result = results[0]
if (!result) {
return { result: undefined, ...data }
}

const getAddressPart = (part: AddressPart | AddressPart[]) => {
if (Array.isArray(part)) {
return (
result.address_components.find(({ types }) => part.some((val) => types.includes(val))) ?? {
short_name: undefined,
long_name: undefined,
types: [],
}
)
}
const getAddressPart = (part: AddressPart | AddressPart[]) => {
if (Array.isArray(part)) {
return (
result.address_components.find(({ types }) => types.includes(part)) ?? {
result.address_components.find(({ types }) => part.some((val) => types.includes(val))) ?? {
short_name: undefined,
long_name: undefined,
types: [],
}
)
}
return (
result.address_components.find(({ types }) => types.includes(part)) ?? {
short_name: undefined,
long_name: undefined,
types: [],
}
)
}

const { short_name: streetNumber } = getAddressPart(GeocodingAddressComponentType.street_number)
const { long_name: streetName } = getAddressPart(PlaceType2.route)
const { long_name: city } = getAddressPart([PlaceType2.locality, PlaceType2.postal_town])
const { short_name: govDist } = getAddressPart(PlaceType2.administrative_area_level_1)
const { short_name: postCode } = getAddressPart(PlaceType2.postal_code)
const { short_name: country } = getAddressPart(PlaceType2.country)
const { short_name: streetNumber } = getAddressPart(GeocodingAddressComponentType.street_number)
const { long_name: streetName } = getAddressPart(PlaceType2.route)
const { long_name: city } = getAddressPart([PlaceType2.locality, PlaceType2.postal_town])
const { short_name: govDist } = getAddressPart(PlaceType2.administrative_area_level_1)
const { short_name: postCode } = getAddressPart(PlaceType2.postal_code)
const { short_name: country } = getAddressPart(PlaceType2.country)

//second line of an address
const { long_name: premise } = getAddressPart(PlaceType2.premise)
const { long_name: subpremise } = getAddressPart(PlaceType2.subpremise)
const street2 = compact([subpremise, premise]).length
? compact([subpremise, premise]).join(', ')
: undefined
//second line of an address
const { long_name: premise } = getAddressPart(PlaceType2.premise)
const { long_name: subpremise } = getAddressPart(PlaceType2.subpremise)
const street2 = compact([subpremise, premise]).length
? compact([subpremise, premise]).join(', ')
: undefined

return {
result: {
geometry: result.geometry,
streetNumber,
streetName,
street2,
city,
govDist,
postCode,
country,
},
...data,
}
})
return {
result: {
geometry: result.geometry,
streetNumber,
streetName,
street2,
city,
govDist,
postCode,
country,
},
...data,
}
})

export const geocodeByStringResponse = GoogleResponse.extend({
results: z
.object({
address_components: z
.object({
long_name: z.string(),
short_name: z.string(),
types: z.array(z.string()),
})
.array(),
formatted_address: z.string(),
geometry: z.object({
bounds: BoundsSchema,
location: coordinates,
location_type: z.string(),
viewport: BoundsSchema,
}),
place_id: z.string(),
types: z.array(z.string()),
})
.array(),
}).transform(({ results, ...rest }) => ({
...rest,
results: results.length === 1 ? results.at(0) : results,
}))

export type AutocompleteResponse = z.infer<typeof autocompleteResponse>
export type GeocodeResponse = z.infer<typeof geocodeResponse>
export type GoogleAPIResponse = AutocompleteResponse | GeocodeResponse
export type GeocodeByPlaceIdResponse = z.infer<typeof geocodeByPlaceIdResponse>
export type GeocodeByStringResponse = z.infer<typeof geocodeByStringResponse>
export type GoogleAPIResponse = AutocompleteResponse | GeocodeByPlaceIdResponse | GeocodeByStringResponse
Loading

0 comments on commit cb86839

Please sign in to comment.