diff --git a/CHANGELOG.md b/CHANGELOG.md index e7f34a58..f9e14557 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,12 @@ way to update this template, but currently, we follow a pattern: --- ## Upcoming version 2018-08-XX +* [change] Add support for default locations in the + LocationAutocompleteInput component. Common searches can be + configured to show when the input has focus. This reduces typing and + Google Places geolocation API usage. The defaults can be configured + in `src/components/LocationAutocompleteInput/GeocoderGoogleMaps.js`. + [#894](https://github.com/sharetribe/flex-template-web/pull/894) * [change] Removed the `country` parameter from the search page as it was not used anywhere. [#893](https://github.com/sharetribe/flex-template-web/pull/893) diff --git a/docs/google-maps.md b/docs/google-maps.md index 01c91d96..4fb336cc 100644 --- a/docs/google-maps.md +++ b/docs/google-maps.md @@ -25,3 +25,14 @@ local development, you can add the variable in the Gitignored `.env` file in the ``` REACT_APP_GOOGLE_MAPS_API_KEY=my-key-here ``` + +## Setup common locations to reduce typing + +The location autocomplete input in the landing page and the topbar can +be configured to have specific locations shown by default when the +user focuses on the input and hasn't yet typed in any searches. This +reduces the typing required for common searches, and also reduces the +need to use the Google Places geolocation API that much. + +The default locations can be configured in +[src/components/LocationAutocompleteInput/GeocoderGoogleMaps.js](../src/components/LocationAutocompleteInput/GeocoderGoogleMaps.js). diff --git a/src/components/LocationAutocompleteInput/GeocoderGoogleMaps.js b/src/components/LocationAutocompleteInput/GeocoderGoogleMaps.js index b8942cd6..d4bba93d 100644 --- a/src/components/LocationAutocompleteInput/GeocoderGoogleMaps.js +++ b/src/components/LocationAutocompleteInput/GeocoderGoogleMaps.js @@ -3,6 +3,30 @@ import { getPlacePredictions, getPlaceDetails } from '../../util/googleMaps'; import css from './LocationAutocompleteInput.css'; +// A list of default predictions that can be shown when the user +// focuses on the autocomplete input without typing a search. This can +// be used to reduce typing and Geocoding API calls for common +// searches. +// +// Example: +// +// [ +// { +// place_id: 'Place ID from Google Maps Places API', +// description: 'Place name to show in the autocomplete dropdown', +// }, +// ] +// +// To know which values to set as defaults, log a real prediction +// object from the Places API call and copy the Place ID from the +// response. +export const defaultPredictions = [ + // { + // place_id: 'ChIJkQYhlscLkkYRY_fiO4S9Ts0', + // description: 'Helsinki, Finland', + // }, +]; + // When displaying data from the Google Maps Places API, and // attribution is required next to the results. // See: https://developers.google.com/places/web-service/policies#powered @@ -44,6 +68,13 @@ class GeocoderGoogleMaps { }); } + /** + * Get the ID of the given prediction. + */ + getPredictionId(prediction) { + return prediction.place_id; + } + /** * Get the address text of the given prediction. */ diff --git a/src/components/LocationAutocompleteInput/GeocoderMapbox.js b/src/components/LocationAutocompleteInput/GeocoderMapbox.js index b5d0495a..471fa18a 100644 --- a/src/components/LocationAutocompleteInput/GeocoderMapbox.js +++ b/src/components/LocationAutocompleteInput/GeocoderMapbox.js @@ -3,6 +3,8 @@ import config from '../../config'; const { LatLng: SDKLatLng, LatLngBounds: SDKLatLngBounds } = sdkTypes; +const placeId = prediction => prediction.id; + const placeAddress = prediction => prediction.place_name; const placeOrigin = prediction => { @@ -24,6 +26,34 @@ const placeBounds = prediction => { return null; }; +// A list of default predictions that can be shown when the user +// focuses on the autocomplete input without typing a search. This can +// be used to reduce typing and Geocoding API calls for common +// searches. +// +// Example: +// +// [ +// { +// id: 'any unique id', +// place_name: 'Place name to show in the autocomplete dropdown', +// center: [longitude, latitude], +// bbox: [minX, minY, maxX, maxY], +// }, +// ] +// +// To know which values to set as defaults, log a real prediction +// object from the Geocoding API call and check the values from the +// response. +export const defaultPredictions = [ + // { + // id: 'default-helsinki', + // place_name: 'Helsinki, Finland', + // center: [24.94861, 60.17333], + // bbox: [24.82617, 60.075361, 25.313112, 60.297839], + // }, +]; + export const GeocoderAttribution = () => null; /** @@ -66,6 +96,13 @@ class GeocoderMapbox { }); } + /** + * Get the ID of the given prediction. + */ + getPredictionId(prediction) { + return placeId(prediction); + } + /** * Get the address text of the given prediction. */ diff --git a/src/components/LocationAutocompleteInput/LocationAutocompleteInputImpl.js b/src/components/LocationAutocompleteInput/LocationAutocompleteInputImpl.js index 930e00e5..816be92e 100644 --- a/src/components/LocationAutocompleteInput/LocationAutocompleteInputImpl.js +++ b/src/components/LocationAutocompleteInput/LocationAutocompleteInputImpl.js @@ -3,7 +3,8 @@ import { any, arrayOf, bool, func, number, shape, string, oneOfType, object } fr import classNames from 'classnames'; import debounce from 'lodash/debounce'; import { propTypes } from '../../util/types'; -import Geocoder, { GeocoderAttribution } from './GeocoderGoogleMaps'; +import Geocoder, { GeocoderAttribution, defaultPredictions } from './GeocoderGoogleMaps'; +// import Geocoder, { GeocoderAttribution, defaultPredictions } from './GeocoderMapbox'; import css from './LocationAutocompleteInput.css'; @@ -69,12 +70,12 @@ const LocationPredictionsList = props => { return (
  • onSelectStart(getTouchCoordinates(e.nativeEvent))} onMouseDown={() => onSelectStart()} onTouchMove={e => onSelectMove(getTouchCoordinates(e.nativeEvent))} - onTouchEnd={() => onSelectEnd(index)} - onMouseUp={() => onSelectEnd(index)} + onTouchEnd={() => onSelectEnd(prediction)} + onMouseUp={() => onSelectEnd(prediction)} > {geocoder.getPredictionAddress(prediction)}
  • @@ -148,8 +149,9 @@ class LocationAutocompleteInputImpl extends Component { this.geocoder = new Geocoder(); + this.currentPredictions = this.currentPredictions.bind(this); this.changeHighlight = this.changeHighlight.bind(this); - this.selectItem = this.selectItem.bind(this); + this.selectPrediction = this.selectPrediction.bind(this); this.selectItemIfNoneSelected = this.selectItemIfNoneSelected.bind(this); this.onKeyDown = this.onKeyDown.bind(this); this.onChange = this.onChange.bind(this); @@ -164,6 +166,13 @@ class LocationAutocompleteInputImpl extends Component { this.predict = debounce(this.predict.bind(this), DEBOUNCE_WAIT_TIME, { leading: true }); } + currentPredictions() { + const { search, predictions: fetchedPredictions } = currentValue(this.props); + const hasFetchedPredictions = fetchedPredictions && fetchedPredictions.length > 0; + + return !search && !hasFetchedPredictions ? defaultPredictions : fetchedPredictions; + } + // Interpret input key event onKeyDown(e) { if (e.keyCode === KEY_CODE_ARROW_UP) { @@ -175,9 +184,9 @@ class LocationAutocompleteInputImpl extends Component { e.preventDefault(); this.changeHighlight(DIRECTION_DOWN); } else if (e.keyCode === KEY_CODE_ENTER) { - const { search, selectedPlace } = currentValue(this.props); + const { selectedPlace } = currentValue(this.props); - if (search && !selectedPlace) { + if (!selectedPlace) { // Prevent form submit, try to select value instead. e.preventDefault(); e.stopPropagation(); @@ -193,7 +202,7 @@ class LocationAutocompleteInputImpl extends Component { // Handle input text change, fetch predictions if the value isn't empty onChange(e) { const onChange = this.props.input.onChange; - const { predictions } = currentValue(this.props); + const predictions = this.currentPredictions(); const newValue = e.target.value; // Clear the current values since the input content is changed @@ -220,7 +229,7 @@ class LocationAutocompleteInputImpl extends Component { // (DIRECTION_UP or DIRECTION_DOWN) changeHighlight(direction) { this.setState((prevState, props) => { - const { predictions } = currentValue(props); + const predictions = this.currentPredictions(); const currentIndex = prevState.highlightedIndex; let index = currentIndex; @@ -244,16 +253,7 @@ class LocationAutocompleteInputImpl extends Component { // Select the prediction in the given item. This will fetch/read the // place details and set it as the selected place. - selectItem(index) { - if (index < 0) { - return; - } - const { predictions } = currentValue(this.props); - if (index >= predictions.length) { - return; - } - const prediction = predictions[index]; - + selectPrediction(prediction) { this.props.input.onChange({ ...this.props.input, selectedPlace: null, @@ -278,10 +278,14 @@ class LocationAutocompleteInputImpl extends Component { }); } selectItemIfNoneSelected() { - const { search, selectedPlace } = currentValue(this.props); - if (search && !selectedPlace) { + const { selectedPlace } = currentValue(this.props); + const predictions = this.currentPredictions(); + if (!selectedPlace) { const index = this.state.highlightedIndex !== -1 ? this.state.highlightedIndex : 0; - this.selectItem(index); + + if (index >= 0 && index < predictions.length) { + this.selectPrediction(predictions[index]); + } } } predict(search) { @@ -350,7 +354,7 @@ class LocationAutocompleteInputImpl extends Component { }); } - handlePredictionsSelectEnd(index) { + handlePredictionsSelectEnd(prediction) { let selectAndFinalize = false; this.setState( prevState => { @@ -361,7 +365,7 @@ class LocationAutocompleteInputImpl extends Component { }, () => { if (selectAndFinalize) { - this.selectItem(index); + this.selectPrediction(prediction); this.finalizeSelection(); } } @@ -383,9 +387,10 @@ class LocationAutocompleteInputImpl extends Component { inputRef, } = this.props; const { name, onFocus } = input; - const { search, predictions } = currentValue(this.props); + const { search } = currentValue(this.props); const { touched, valid } = meta || {}; const isValid = valid && touched; + const predictions = this.currentPredictions(); const handleOnFocus = e => { this.setState({ inputHasFocus: true });