Skip to content

Commit

Permalink
refactor geolocation handler into a hook
Browse files Browse the repository at this point in the history
  • Loading branch information
Yen Truong committed Nov 17, 2022
1 parent a74100f commit 09247fe
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 45 deletions.
54 changes: 9 additions & 45 deletions src/components/Geolocation.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { Matcher, SelectableStaticFilter, useSearchActions, useSearchState } from '@yext/search-headless-react';
import { executeSearch } from '../utils/search-operations';
import { getUserLocation } from '../utils/location-operations';
import { useComposedCssClasses } from '../hooks/useComposedCssClasses';
import { useCallback, useState } from 'react';
import { useState } from 'react';
import LoadingIndicator from '../icons/LoadingIndicator';
import { YextIcon } from '../icons/YextIcon';
import { useGeolocationHandler } from '../hooks/useGeolocationHandler';

/**
* The CSS class interface for the Geolocation component.
Expand Down Expand Up @@ -52,9 +50,6 @@ export interface GeolocationProps {
customCssClasses?: GeolocationCssClasses
}

const LOCATION_FIELD_ID = 'builtin.location';
const METERS_PER_MILE = 1609.344;

/**
* A React Component which collects location information to create a
* location filter and perform a new search.
Expand All @@ -73,46 +68,15 @@ export function Geolocation({
handleClick,
customCssClasses,
}: GeolocationProps): JSX.Element | null {
const searchActions = useSearchActions();
const staticFilters = useSearchState(s => s.filters.static || []);
const [isFetchingLocation, setIsFetchingLocation] = useState<boolean>(false);
const cssClasses = useComposedCssClasses(builtInCssClasses, customCssClasses);

const defaultHandleClick = useCallback((position: GeolocationPosition) => {
const { latitude, longitude, accuracy } = position.coords;
const locationFilter: SelectableStaticFilter = {
displayName: 'Current Location',
selected: true,
filter: {
kind: 'fieldValue',
fieldId: LOCATION_FIELD_ID,
matcher: Matcher.Near,
value: {
lat: latitude,
lng: longitude,
radius: Math.max(accuracy, radius * METERS_PER_MILE)
},
}
};
const nonLocationFilters = staticFilters.filter(filter => {
return !(filter.filter.kind === 'fieldValue'
&& filter.filter.fieldId === LOCATION_FIELD_ID);
});
searchActions.setStaticFilters([...nonLocationFilters, locationFilter]);
executeSearch(searchActions);
}, [radius, searchActions, staticFilters]);

const handleGeolocationClick = useCallback(async () => {
setIsFetchingLocation(true);
try {
const position = await getUserLocation(geolocationOptions);
(handleClick ?? defaultHandleClick)(position);
} catch (e) {
console.warn(e);
} finally {
setIsFetchingLocation(false);
}
}, [geolocationOptions, handleClick, defaultHandleClick]);
const handleGeolocationClick = useGeolocationHandler({
setIsFetchingLocation,
geolocationOptions,
radius,
handleUserPosition: handleClick
});

return (
<div className={cssClasses.geolocationContainer}>
Expand All @@ -124,4 +88,4 @@ export function Geolocation({
</div>
</div>
);
}
}
82 changes: 82 additions & 0 deletions src/hooks/useGeolocationHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@

import { Matcher, SelectableStaticFilter, useSearchActions, useSearchState } from '@yext/search-headless-react';
import { executeSearch } from '../utils/search-operations';
import { getUserLocation } from '../utils/location-operations';
import { useCallback } from 'react';

const LOCATION_FIELD_ID = 'builtin.location';
const METERS_PER_MILE = 1609.344;

/**
* The props for {@link useGeolocationHandler} hook.
*
* @internal
*/
interface GeolocationhandlerProps {
/** A React dispatch function use to update the status of fetching user's location. */
setIsFetchingLocation: React.Dispatch<React.SetStateAction<boolean>>,
/** Configuration used when collecting the user's location. */
geolocationOptions?: PositionOptions,
/**
* The radius, in miles, around the user's location to find results. Defaults to 50.
* If location accuracy is low, a larger radius may be used automatically.
*/
radius?: number,
/** Custom handler function to call after user's position is successfully determined. */
handleUserPosition?: (position: GeolocationPosition) => void
}

/**
* Returns a function that will collect user's geolocation and, by default, will set
* a built-in location filter and execute a search.
*
* @internal
*
* @param props - {@link GeolocationProps}
* @returns A function to collect and process user's geolocation
*/
export function useGeolocationHandler({
setIsFetchingLocation,
geolocationOptions,
radius = 50,
handleUserPosition
}: GeolocationhandlerProps): () => Promise<void> {
const searchActions = useSearchActions();
const staticFilters = useSearchState(s => s.filters.static || []);

const defaultHandleUserPosition = useCallback((position: GeolocationPosition) => {
const { latitude, longitude, accuracy } = position.coords;
const locationFilter: SelectableStaticFilter = {
displayName: 'Current Location',
selected: true,
filter: {
kind: 'fieldValue',
fieldId: LOCATION_FIELD_ID,
matcher: Matcher.Near,
value: {
lat: latitude,
lng: longitude,
radius: Math.max(accuracy, radius * METERS_PER_MILE)
},
}
};
const nonLocationFilters = staticFilters.filter(filter => {
return !(filter.filter.kind === 'fieldValue'
&& filter.filter.fieldId === LOCATION_FIELD_ID);
});
searchActions.setStaticFilters([...nonLocationFilters, locationFilter]);
executeSearch(searchActions);
}, [radius, searchActions, staticFilters]);

return useCallback(async () => {
setIsFetchingLocation(true);
try {
const position = await getUserLocation(geolocationOptions);
(handleUserPosition ?? defaultHandleUserPosition)(position);
} catch (e) {
console.warn(e);
} finally {
setIsFetchingLocation(false);
}
}, [setIsFetchingLocation, geolocationOptions, handleUserPosition, defaultHandleUserPosition]);
}

0 comments on commit 09247fe

Please sign in to comment.