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"