diff --git a/package.json b/package.json index bd13402..2af9194 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "normalize-scss": "^6.0.0", "query-string": "^5.0.1", "react": "^15.4.2", + "react-autosuggest": "^9.3.4", "react-collapse": "^2.3.3", "react-copy-to-clipboard": "^4.2.3", "react-dom": "^15.4.2", diff --git a/scss/components/_search-results.scss b/scss/components/_search-results.scss new file mode 100644 index 0000000..5cbf615 --- /dev/null +++ b/scss/components/_search-results.scss @@ -0,0 +1,32 @@ +.SearchResults { + width: 100%; + position: relative; + padding-right: 10px; + + border: 1px solid $marine-light; + + p { + margin-bottom: 10px; + } + + p.address { + font-size: 14px; + } + + button.close { + float: right; + + height: 20px; + padding: 0 5px; + margin: 5px 5px 10px 10px; + font-size: 11px; + line-height: 11px; + background-color: $off-white; + border-color: transparent; + color: $marine-light; + text-align: left; + text-transform: capitalize; + letter-spacing: 0.5px; + border: none; + } +} diff --git a/scss/components/_search.scss b/scss/components/_search.scss new file mode 100644 index 0000000..1d59151 --- /dev/null +++ b/scss/components/_search.scss @@ -0,0 +1,73 @@ +.Search { + width: 240px; + position: absolute; + top: $margin-5 + $app-header-height - 10; + left: $margin-25 + $zoom-controls-width + $padding-10 - 10; + z-index: 5; + padding-right: 10px; + background-color: transparent; + + .status { + line-height: 52px; + } + + .react-autosuggest__container { + position: relative; + } + + .react-autosuggest__input { + width: 240px; + height: 30px; + padding: 10px 20px; + font-family: Helvetica, sans-serif; + font-weight: 300; + font-size: 16px; + border: 1px solid #aaa; + border-radius: 4px; + } + + .react-autosuggest__input--focused { + outline: none; + } + + .react-autosuggest__input--open { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + + .react-autosuggest__suggestions-container { + display: none; + } + + .react-autosuggest__suggestions-container--open { + display: block; + position: absolute; + top: 51px; + width: 280px; + border: 1px solid #aaa; + background-color: #fff; + font-family: Helvetica, sans-serif; + font-weight: 300; + font-size: 16px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + z-index: 2; + } + + .react-autosuggest__suggestions-list { + margin: 0; + padding: 0; + list-style-type: none; + } + + .react-autosuggest__suggestion { + cursor: pointer; + padding: 5px 10px; + color: #333; + } + + .react-autosuggest__suggestion--highlighted { + background-color: #ddd; + } + +} diff --git a/scss/main.scss b/scss/main.scss index 254a2e9..209e2c6 100644 --- a/scss/main.scss +++ b/scss/main.scss @@ -37,3 +37,5 @@ @import 'components/modal'; @import 'components/small-device-message'; @import 'components/about-copy'; +@import 'components/search'; +@import 'components/search-results'; diff --git a/src/actions/async_actions.js b/src/actions/async_actions.js index 5a474e5..714aeec 100644 --- a/src/actions/async_actions.js +++ b/src/actions/async_actions.js @@ -12,6 +12,15 @@ import { polyfill(); +// fetch() has an annoying way of handling http errors, so make sure to check for them +function maybeHandleHTTPError (response) { + if (!response.ok) { + throw Error(response.statusText); + } else { + return response; + } +} + const requestCrashStatsData = () => ({ type: actions.CRASHES_ALL_REQUEST }); @@ -32,6 +41,7 @@ export const fetchCrashStatsData = (params) => { return (dispatch) => { dispatch(requestCrashStatsData()); return fetch(url) + .then(maybeHandleHTTPError) .then(res => res.json()) .then(json => dispatch(receiveCrashStatsData(json.rows[0]))) .catch(error => dispatch(receiveCrashStatsError(error))); @@ -58,6 +68,7 @@ export const fetchContributingFactors = (params) => { return (dispatch) => { dispatch(requestContributingFactors()); return fetch(url) + .then(maybeHandleHTTPError) .then(res => res.json()) .then(json => dispatch(receiveContributingFactors(json.rows))) .catch(error => dispatch(receiveContributingFactorsError(error))); @@ -84,6 +95,7 @@ export const fetchCrashesYearRange = () => { return (dispatch) => { dispatch(requestCrashesYearRange()); return fetch(url) + .then(maybeHandleHTTPError) .then(res => res.json()) .then(json => dispatch(receiveCrashesYearRange(json.rows))) .catch(error => dispatch(receiveCrashesYearRangeError(error))); @@ -110,6 +122,7 @@ export const fetchCrashesDateRange = () => { return (dispatch) => { dispatch(requestCrashesDateRange()); return fetch(url) + .then(maybeHandleHTTPError) .then(res => res.json()) .then(json => dispatch(receiveCrashesDateRange(json.rows))) .catch(error => dispatch(receiveCrashesDateRangeError(error))); @@ -136,6 +149,7 @@ export const fetchCrashesMaxDate = () => { return (dispatch) => { dispatch(requestCrashesMaxDate()); return fetch(url) + .then(maybeHandleHTTPError) .then(res => res.json()) .then(json => dispatch(receiveCrashesMaxDate(json.rows))) .catch(error => dispatch(receiveCrashesMaxDateError(error))); @@ -162,6 +176,7 @@ export const fetchGeoPolygons = (geo) => { return (dispatch) => { dispatch(requestGeoPolygons()); return fetch(url) + .then(maybeHandleHTTPError) .then(res => res.json()) .then((json) => { // tack on the geography name so that it may be diff'd in LeafletMap propTypes diff --git a/src/actions/filter_by_search_location.js b/src/actions/filter_by_search_location.js new file mode 100644 index 0000000..c8f5a1e --- /dev/null +++ b/src/actions/filter_by_search_location.js @@ -0,0 +1,15 @@ +import { + FILTER_BY_LOCATION, + CLEAR_FILTER_BY_LOCATION, +} from '../constants/action_types'; + +export const filterByLocation = coordinates => ({ + type: FILTER_BY_LOCATION, + coordinates +}); + +export const clearFilterByLocation = () => ({ + type: CLEAR_FILTER_BY_LOCATION, +}); + +export default filterByLocation; diff --git a/src/actions/index.js b/src/actions/index.js index ccc4bd4..0dffc16 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -4,6 +4,7 @@ export { fetchCrashStatsData, fetchCrashesDateRange, fetchCrashesMaxDate, fetchGeoPolygons, + fetchLocationGeocode } from './async_actions'; export { startDateChange, endDateChange } from './filter_by_date_actions'; export { @@ -19,3 +20,5 @@ export { } from './filter_by_area_actions'; export filterByContributingFactor from './filter_contributing_factor_actions'; export { openModal, closeModal } from './modal_actions'; +export * from './location_search_actions'; +export * from './filter_by_search_location'; diff --git a/src/actions/location_search_actions.js b/src/actions/location_search_actions.js new file mode 100644 index 0000000..627733f --- /dev/null +++ b/src/actions/location_search_actions.js @@ -0,0 +1,67 @@ +import fetch from 'isomorphic-fetch'; +import { polyfill } from 'es6-promise'; +import { + LOCATION_SEARCH_REQUEST, + LOCATION_SEARCH_SUCCESS, + LOCATION_SEARCH_ERROR, + CLEAR_SEARCH_SUGGESTIONS, + UPDATE_AUTOSUGGEST_VALUE, + LOCATION_SEARCH_SELECT, + RESET_LOCATION_SEARCH, +} from '../constants/action_types'; + +polyfill(); + +export const requestSearchResults = () => ({ + type: LOCATION_SEARCH_REQUEST +}); + +export const receiveSearchResults = payload => ({ + type: LOCATION_SEARCH_SUCCESS, + payload +}); + +export const receiveSearchError = error => ({ + type: LOCATION_SEARCH_ERROR, + error +}); + +export const fetchSearchResults = () => { + const url = 'https://geosearch.planninglabs.nyc/v1/autocomplete?text='; + return (dispatch, getState) => { + const { autosuggestValue } = getState().search; + dispatch(requestSearchResults()); + return fetch(`${url}${autosuggestValue}`) + .then((res) => { + // fetch has an annoying way of handling http errors... + if (!res.ok) { + throw Error(res.statusText); + } else { + return res.json(); + } + }) + .then((json) => { + const payload = json.features; + return dispatch(receiveSearchResults(payload)); + }) + .catch(error => dispatch(receiveSearchError(error.message))); + }; +}; + +export const updateAutosuggestValue = value => ({ + type: UPDATE_AUTOSUGGEST_VALUE, + value +}); + +export const selectSearchResult = feature => ({ + type: LOCATION_SEARCH_SELECT, + feature +}); + +export const clearSearchSuggestions = () => ({ + type: CLEAR_SEARCH_SUGGESTIONS +}); + +export const resetLocationSearch = () => ({ + type: RESET_LOCATION_SEARCH +}); diff --git a/src/components/App.js b/src/components/App.js index f56d9dd..cd8e566 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -10,6 +10,7 @@ import OptionsFiltersConnected from '../containers/OptionsFiltersConnected'; import ModalConnected from '../containers/ModalConnected'; import SmallDeviceMessage from './SmallDeviceMessage'; import LoadingIndicator from './LoadingIndicator'; +import Search from '../containers/SearchConnected'; class App extends Component { constructor() { @@ -96,6 +97,7 @@ class App extends Component { : [ , + , ${label}

`) + .addTo(this.map); + this.searchCircle = L.circle(coordinates, + intersectionCircleRadiusMeters, + intersectionCircleStyle) + .addTo(this.map); + } + + // remove marker if user cleared search result or applied filter by location + if (!searchResult && this.props.searchResult) { + this.map.removeLayer(this.searchMarker); + this.map.removeLayer(this.searchCircle); + this.searchMarker = null; + this.searchCircle = null; + } } shouldComponentUpdate() { @@ -351,10 +384,7 @@ class LeafletMap extends Component { function handleMouseover(e) { const layer = e.target; - layer.setStyle({ - fillColor: '#105b63', - fillOpacity: 1 - }); + layer.setStyle(geoPolygonStyle); self.revealFilterAreaTooltip(geo, e); @@ -454,6 +484,8 @@ LeafletMap.defaultProps = { identifier: '', lngLats: [], geojson: {}, + searchResult: null, + filterCoords: [], }; LeafletMap.propTypes = { @@ -494,6 +526,12 @@ LeafletMap.propTypes = { }).isRequired, noInjuryFatality: PropTypes.bool.isRequired }).isRequired, + filterCoords: PropTypes.arrayOf(PropTypes.number), + searchResult: PropTypes.shape({ + properties: PropTypes.object, + geometry: PropTypes.object, + type: PropTypes.string + }), }; export default LeafletMap; diff --git a/src/components/Search/SearchResults.js b/src/components/Search/SearchResults.js new file mode 100644 index 0000000..8b9be45 --- /dev/null +++ b/src/components/Search/SearchResults.js @@ -0,0 +1,64 @@ +import React, { Component, PropTypes } from 'react'; + +import { intersectionCircleRadiusFeet } from '../../constants/app_config'; + +class SearchResults extends Component { + static propTypes = { + resetLocationSearch: PropTypes.func.isRequired, + error: PropTypes.string, + filterByLocation: PropTypes.func.isRequired, + isFetching: PropTypes.bool.isRequired, + selectedFeature: PropTypes.shape({}), + } + + static defaultProps = { + error: null, + result: null, + selectedFeature: null + } + + handleFilterResult = (e) => { + e.preventDefault(); + const { selectedFeature } = this.props; + this.props.filterByLocation(selectedFeature.coordinates); + } + + closeThisPanel = (e) => { + e.preventDefault(); + this.props.resetLocationSearch(); + } + + showSearchResult = () => { + const { error, isFetching, selectedFeature } = this.props; + + if (isFetching) { + return (

searching...

); + } else if (error) { + return (

{error}

); + } else if (selectedFeature) { + return [ + , +

Filter crashes within {intersectionCircleRadiusFeet} feet of this location?

, +

{selectedFeature.properties.label}

, + + ]; + } + return null; + } + + render() { + return ( +
+ {this.showSearchResult()} +
+ ); + } +} + +export default SearchResults; diff --git a/src/components/Search/index.js b/src/components/Search/index.js new file mode 100644 index 0000000..7499c1c --- /dev/null +++ b/src/components/Search/index.js @@ -0,0 +1,107 @@ +import React, { PropTypes } from 'react'; +import throttle from 'lodash/throttle'; +import Autosuggest from 'react-autosuggest'; + +import SearchResults from './SearchResults'; + +// don't overwhelm the geocoding API +const THROTTLE_WAIT_MS = 200; + +// TODO: implement filter by location +const noop = () => {}; + +const Search = ({ + autosuggestValue, + suggestions, + fetchSearchResults, + updateAutosuggestValue, + clearSearchSuggestions, + selectSearchResult, + resetLocationSearch, + selectedFeature, + error, + isFetching +}) => { + const onSuggestionsFetchRequested = () => { + fetchSearchResults(); + }; + + const onSuggestionsClearRequested = () => { + clearSearchSuggestions(); + }; + + const handleChange = (event, { newValue }) => { + updateAutosuggestValue(newValue); + }; + + const onSuggestionSelected = () => { + const result = suggestions.length ? suggestions[0] : null; + selectSearchResult(result); + }; + + // eslint-disable-next-line + const getSuggestionValue = suggestion => + suggestion && suggestion.properties && suggestion.properties.name + ? suggestion.properties.name + : null; + + // eslint-disable-next-line + const renderSuggestion = feature => + feature && feature.properties && feature.properties.name ? ( + {feature.properties.name} + ) : null; + + const inputProps = { + placeholder: 'Search an NYC address...', + value: autosuggestValue, + onChange: handleChange + }; + + return ( +
+ + { + (selectedFeature || error) && + + } +
+ ); +}; + +Search.propTypes = { + error: PropTypes.string, + isFetching: PropTypes.bool.isRequired, + autosuggestValue: PropTypes.string.isRequired, + suggestions: PropTypes.arrayOf(PropTypes.object).isRequired, + fetchSearchResults: PropTypes.func.isRequired, + updateAutosuggestValue: PropTypes.func.isRequired, + clearSearchSuggestions: PropTypes.func.isRequired, + selectSearchResult: PropTypes.func.isRequired, + resetLocationSearch: PropTypes.func.isRequired, + selectedFeature: PropTypes.shape({ + geometry: PropTypes.object, + properties: PropTypes.object, + type: PropTypes.string + }) +}; + +Search.defaultProps = { + error: null, + selectedFeature: null +}; + +export default Search; diff --git a/src/constants/action_types.js b/src/constants/action_types.js index 2e513be..042e751 100644 --- a/src/constants/action_types.js +++ b/src/constants/action_types.js @@ -5,6 +5,9 @@ export const FILTER_BY_AREA_TYPE = 'FILTER_BY_AREA_TYPE'; export const FILTER_BY_AREA_IDENTIFIER = 'FILTER_BY_AREA_IDENTIFIER'; export const FILTER_BY_AREA_CUSTOM = 'FILTER_BY_AREA_CUSTOM'; +export const FILTER_BY_LOCATION = 'FILTER_BY_LOCATION'; +export const CLEAR_FILTER_BY_LOCATION = 'CLEAR_FILTER_BY_LOCATION'; + export const TOGGLE_CUSTOM_AREA_DRAW = 'TOGGLE_CUSTOM_AREA_DRAW'; export const FILTER_BY_TYPE_INJURY = 'FILTER_BY_TYPE_INJURY'; @@ -39,3 +42,11 @@ export const GEO_POLYGONS_ERROR = 'GEO_POLYGONS_ERROR'; export const MODAL_OPENED = 'MODAL_OPENED'; export const MODAL_CLOSED = 'MODAL_CLOSED'; + +export const LOCATION_SEARCH_REQUEST = 'LOCATION_SEARCH_REQUEST'; +export const LOCATION_SEARCH_SUCCESS = 'LOCATION_SEARCH_SUCCESS'; +export const LOCATION_SEARCH_ERROR = 'LOCATION_SEARCH_ERROR'; +export const LOCATION_SEARCH_SELECT = 'LOCATION_SEARCH_SELECT'; +export const UPDATE_AUTOSUGGEST_VALUE = 'UPDATE_AUTOSUGGEST_VALUE'; +export const CLEAR_SEARCH_SUGGESTIONS = 'CLEAR_SEARCH_SUGGESTIONS'; +export const RESET_LOCATION_SEARCH = 'RESET_LOCATION_SEARCH'; diff --git a/src/constants/app_config.js b/src/constants/app_config.js index 8896728..915e1d8 100644 --- a/src/constants/app_config.js +++ b/src/constants/app_config.js @@ -57,3 +57,25 @@ export const labelFormats = { nypd_precinct: 'NYPD Precinct {}', intersection: '{}', }; + +// GeoJSON polyfons for clicking specific areas +// the style for drawing them onto the map +export const geoPolygonStyle = { + fillColor: '#105b63', + fillOpacity: 1 +}; + +// geocoder search: the radius of the "preview" circle, and its visual style +export const intersectionCircleRadiusMeters = 27.4; +export const intersectionCircleRadiusFeet = Math.round(intersectionCircleRadiusMeters * 3.28084); +export const intersectionCircleStyle = { + /* + color: '#105b63', + opacity: 1, + weight: 2, + */ + stroke: false, + fillColor: '#105b63', + fillOpacity: 0.5, + clickable: false, +}; diff --git a/src/constants/sql_queries.js b/src/constants/sql_queries.js index c6c205b..8d08644 100644 --- a/src/constants/sql_queries.js +++ b/src/constants/sql_queries.js @@ -1,6 +1,6 @@ import sls from 'single-line-string'; -import { cartoTables } from './app_config'; +import { cartoTables, intersectionCircleRadiusMeters } from './app_config'; const { nyc_borough, nyc_city_council, @@ -221,6 +221,30 @@ const filterByCustomAreaClause = (lonLatArray) => { return ''; }; +// Creates the WHERE clause for filtering crashes by a radius from a lat lon coordinate pair +// @param {array} filterCoords, an array of two coordinates [lon, lat] +const filterByLocationSQL = (filterCoords) => { + if (filterCoords.length) { + return sls` + AND + ST_Contains( + ST_Buffer( + ST_Transform( + ST_SetSRID( + ST_MakePoint(${filterCoords[0]}, ${filterCoords[1]}), + 4326 + ), + 3785 + ), + ${intersectionCircleRadiusMeters} + ), + c.the_geom_webmercator + ) + `; + } + return ''; +}; + /* ********************************** MAP **************************************** */ @@ -232,7 +256,7 @@ const filterByCustomAreaClause = (lonLatArray) => { // @param {string} harm: crash type, one of 'ALL', 'cyclist', 'motorist', 'ped' // @param {string} persona: crash type, of of 'ALL', 'fatality', 'injury', 'no inj/fat' export const configureMapSQL = (params) => { - const { startDate, endDate, filterType, geo, identifier, lngLats } = params; + const { startDate, endDate, filterType, geo, identifier, lngLats, filterCoords } = params; return sls` SELECT * FROM @@ -259,6 +283,7 @@ export const configureMapSQL = (params) => { ${filterByCustomAreaClause(lngLats)} ${filterByTypeWhereClause(filterType)} ${filterByIdentifierWhereClause(identifier, geo)} + ${filterByLocationSQL(filterCoords)} AND c.the_geom IS NOT NULL GROUP BY diff --git a/src/containers/LeafletMapConnected.js b/src/containers/LeafletMapConnected.js index aef306f..09eb304 100644 --- a/src/containers/LeafletMapConnected.js +++ b/src/containers/LeafletMapConnected.js @@ -1,13 +1,22 @@ import { connect } from 'react-redux'; -import { filterByAreaIdentifier, filterByAreaCustom, fetchGeoPolygons } from '../actions/'; +import { + filterByAreaIdentifier, + filterByAreaCustom, + fetchGeoPolygons +} from '../actions/'; import LeafletMap from '../components/LeafletMap/'; -const mapStateToProps = ({ filterDate, filterType, filterArea }, ownProps) => { +const mapStateToProps = ( + { filterDate, filterType, filterLocation, filterArea, search }, + ownProps +) => { const { startDate, endDate } = filterDate; const { location: { query } } = ownProps; const { lat, lng, zoom } = query; const { geo, geojson, identifier, lngLats, drawEnabeled } = filterArea; + const { filterCoords } = filterLocation; + const { selectedFeature } = search; return { zoom: zoom ? Number(zoom) : undefined, lat: lat ? Number(lat) : undefined, @@ -15,16 +24,18 @@ const mapStateToProps = ({ filterDate, filterType, filterArea }, ownProps) => { startDate, endDate, filterType, + filterCoords, drawEnabeled, geo, geojson, identifier, lngLats, + searchResult: selectedFeature }; }; export default connect(mapStateToProps, { filterByAreaIdentifier, filterByAreaCustom, - fetchGeoPolygons, + fetchGeoPolygons })(LeafletMap); diff --git a/src/containers/SearchConnected.js b/src/containers/SearchConnected.js new file mode 100644 index 0000000..8476217 --- /dev/null +++ b/src/containers/SearchConnected.js @@ -0,0 +1,29 @@ +import { connect } from 'react-redux'; + +import { + fetchSearchResults, + updateAutosuggestValue, + clearSearchSuggestions, + selectSearchResult, + resetLocationSearch, +} from '../actions'; + +import Search from '../components/Search'; + +const mapStateToProps = ({ + search: { error, isFetching, suggestions, autosuggestValue, selectedFeature } +}) => ({ + error, + isFetching, + autosuggestValue, + suggestions, + selectedFeature +}); + +export default connect(mapStateToProps, { + fetchSearchResults, + updateAutosuggestValue, + clearSearchSuggestions, + selectSearchResult, + resetLocationSearch, +})(Search); diff --git a/src/reducers/filter_by_location_search.js b/src/reducers/filter_by_location_search.js new file mode 100644 index 0000000..76921c9 --- /dev/null +++ b/src/reducers/filter_by_location_search.js @@ -0,0 +1,27 @@ +import { + FILTER_BY_LOCATION, + CLEAR_FILTER_BY_LOCATION, +} from '../constants/action_types'; + +const defaultState = { + filterCoords: [] +}; + +export default function (state = defaultState, action) { + switch (action.type) { + case FILTER_BY_LOCATION: + return { + ...state, + filterCoords: action.coordinates + }; + + case CLEAR_FILTER_BY_LOCATION: + return { + ...state, + filterCoords: [] + }; + + default: + return state; + } +} diff --git a/src/reducers/index.js b/src/reducers/index.js index 8f56360..25664b7 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -12,6 +12,8 @@ import crashesDateRange from './crashes_date_range_reducer'; import crashesMaxDate from './crashes_max_date_reducer'; import crashStats from './crash_stats_reducer'; import modal from './modal_reducer'; +import search from './location_search_reducer'; +import filterLocation from './filter_by_location_search'; // breakpoints for redux-responsive store // taken from scss/skeleton/base/variables @@ -38,9 +40,11 @@ const rootReducer = combineReducers({ filterDate, filterArea, filterContributingFactor, + filterLocation, filterType, modal, routing: routerReducer, + search, yearRange, }); diff --git a/src/reducers/location_search_reducer.js b/src/reducers/location_search_reducer.js new file mode 100644 index 0000000..1627414 --- /dev/null +++ b/src/reducers/location_search_reducer.js @@ -0,0 +1,67 @@ +import { + LOCATION_SEARCH_REQUEST, + LOCATION_SEARCH_SUCCESS, + LOCATION_SEARCH_ERROR, + CLEAR_SEARCH_SUGGESTIONS, + UPDATE_AUTOSUGGEST_VALUE, + LOCATION_SEARCH_SELECT, + RESET_LOCATION_SEARCH, +} from '../constants/action_types'; + +const defaultState = { + error: null, + isFetching: false, + suggestions: [], + autosuggestValue: '', + selectedFeature: null, +}; + +export default function (state = defaultState, action) { + switch (action.type) { + case LOCATION_SEARCH_REQUEST: + return { + ...state, + isFetching: true + }; + + case LOCATION_SEARCH_SUCCESS: + return { + ...state, + isFetching: false, + suggestions: action.payload + }; + + case LOCATION_SEARCH_ERROR: + return { + ...state, + isFetching: false, + error: action.error + }; + + case UPDATE_AUTOSUGGEST_VALUE: + return { + ...state, + autosuggestValue: action.value + }; + + case LOCATION_SEARCH_SELECT: + return { + ...state, + selectedFeature: action.feature + }; + + case CLEAR_SEARCH_SUGGESTIONS: + return { + ...state, + suggestions: [] + }; + + case RESET_LOCATION_SEARCH: + return { + ...defaultState + }; + + default: + return state; + } +} diff --git a/webpack.config.js b/webpack.config.js index 54b2a10..5151cf0 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -13,6 +13,7 @@ const VENDOR_LIBS = [ 'normalize-scss', 'query-string', 'react', + 'react-autosuggest', 'react-collapse', 'react-copy-to-clipboard', 'react-dom', diff --git a/yarn.lock b/yarn.lock index 779c622..861c3e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2437,6 +2437,18 @@ fbjs@^0.8.1, fbjs@^0.8.4: setimmediate "^1.0.5" ua-parser-js "^0.7.9" +fbjs@^0.8.16: + version "0.8.16" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db" + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.9" + fd-slicer@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" @@ -3751,7 +3763,7 @@ longest@^1.0.0, longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" dependencies: @@ -4140,7 +4152,7 @@ object-assign@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" -object-assign@^4.0.0, object-assign@^4.0.1, object-assign@^4.1.0: +object-assign@^4.0.0, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -4693,6 +4705,14 @@ promise@^7.1.1: dependencies: asap "~2.0.3" +prop-types@^15.5.10, prop-types@^15.5.8: + version "15.6.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca" + dependencies: + fbjs "^0.8.16" + loose-envify "^1.3.1" + object-assign "^4.1.1" + proxy-addr@~1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.3.tgz#dc97502f5722e888467b3fa2297a7b1ff47df074" @@ -4791,6 +4811,22 @@ rc@^1.1.2, rc@~1.1.6: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-autosuggest@^9.3.4: + version "9.3.4" + resolved "https://registry.yarnpkg.com/react-autosuggest/-/react-autosuggest-9.3.4.tgz#e47ff800081b2f7c678165bfb7cc84b07f462336" + dependencies: + prop-types "^15.5.10" + react-autowhatever "^10.1.0" + shallow-equal "^1.0.0" + +react-autowhatever@^10.1.0: + version "10.1.1" + resolved "https://registry.yarnpkg.com/react-autowhatever/-/react-autowhatever-10.1.1.tgz#ee938c068d38fc2b7781755e663032a30ed53656" + dependencies: + prop-types "^15.5.8" + react-themeable "^1.1.0" + section-iterator "^2.0.0" + react-collapse@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/react-collapse/-/react-collapse-2.3.3.tgz#68c70f1fceeaf375e3599b95710cc51e5dd339a3" @@ -4869,6 +4905,12 @@ react-select@^1.0.0-rc.3: classnames "^2.2.4" react-input-autosize "^1.1.0" +react-themeable@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/react-themeable/-/react-themeable-1.1.0.tgz#7d4466dd9b2b5fa75058727825e9f152ba379a0e" + dependencies: + object-assign "^3.0.0" + react@^15.4.2: version "15.4.2" resolved "https://registry.yarnpkg.com/react/-/react-15.4.2.tgz#41f7991b26185392ba9bae96c8889e7e018397ef" @@ -5212,6 +5254,10 @@ sax@~1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828" +section-iterator@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/section-iterator/-/section-iterator-2.0.0.tgz#bf444d7afeeb94ad43c39ad2fb26151627ccba2a" + seek-bzip@^1.0.3: version "1.0.5" resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.5.tgz#cfe917cb3d274bcffac792758af53173eb1fabdc" @@ -5305,6 +5351,10 @@ sha.js@^2.3.6: dependencies: inherits "^2.0.1" +shallow-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.0.0.tgz#508d1838b3de590ab8757b011b25e430900945f7" + shelljs@^0.7.5: version "0.7.7" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.7.tgz#b2f5c77ef97148f4b4f6e22682e10bba8667cff1"