2019-02-11 04:04:19 -05:00
|
|
|
|
import React, { Component } from 'react';
|
2019-05-27 13:26:29 -04:00
|
|
|
|
import PropTypes from 'prop-types';
|
2019-02-11 04:04:19 -05:00
|
|
|
|
|
|
|
|
|
import './search-box.css';
|
2019-06-19 06:35:03 -04:00
|
|
|
|
import { SearchIcon } from './icons';
|
2019-02-11 04:04:19 -05:00
|
|
|
|
/**
|
|
|
|
|
* Search for location
|
|
|
|
|
*/
|
2019-08-09 13:49:43 -04:00
|
|
|
|
class SearchBox extends Component<any, any> { // TODO: add proper types
|
|
|
|
|
static propTypes = { // TODO: generate propTypes from TS
|
|
|
|
|
onLocate: PropTypes.func,
|
|
|
|
|
isBuilding: PropTypes.bool
|
|
|
|
|
};
|
|
|
|
|
|
2019-02-11 04:04:19 -05:00
|
|
|
|
constructor(props) {
|
|
|
|
|
super(props);
|
|
|
|
|
this.state = {
|
2019-05-27 11:31:48 -04:00
|
|
|
|
q: '',
|
2019-02-11 04:04:19 -05:00
|
|
|
|
results: [],
|
2019-06-19 06:35:03 -04:00
|
|
|
|
fetching: false,
|
|
|
|
|
//track the state of the search box i.e. collapsed or expanded. Default to true
|
|
|
|
|
collapsedSearch: true,
|
|
|
|
|
//is this a small screen device? if not we will disable collapse option
|
|
|
|
|
smallScreen: false
|
2019-02-11 04:04:19 -05:00
|
|
|
|
}
|
|
|
|
|
this.handleChange = this.handleChange.bind(this);
|
|
|
|
|
this.search = this.search.bind(this);
|
2019-03-19 11:25:17 -04:00
|
|
|
|
this.handleKeyPress = this.handleKeyPress.bind(this);
|
|
|
|
|
this.clearResults = this.clearResults.bind(this);
|
|
|
|
|
this.clearQuery = this.clearQuery.bind(this);
|
2019-06-19 06:35:03 -04:00
|
|
|
|
this.expandSearch = this.expandSearch.bind(this);
|
|
|
|
|
this.onResize= this.onResize.bind(this);
|
2019-02-11 04:04:19 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update search term
|
|
|
|
|
handleChange(e) {
|
|
|
|
|
this.setState({
|
|
|
|
|
q: e.target.value
|
2019-03-19 11:25:17 -04:00
|
|
|
|
});
|
2019-05-27 13:26:29 -04:00
|
|
|
|
// If the ‘clear’ icon has been clicked, clear results list as well
|
2019-05-27 11:31:48 -04:00
|
|
|
|
if(e.target.value === '') {
|
2019-05-27 11:39:16 -04:00
|
|
|
|
this.clearResults();
|
2019-03-19 11:25:17 -04:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear search results on ESC
|
|
|
|
|
handleKeyPress(e){
|
|
|
|
|
if(e.keyCode === 27) {
|
|
|
|
|
//ESC is pressed
|
|
|
|
|
this.clearQuery();
|
|
|
|
|
this.clearResults();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
clearResults(){
|
|
|
|
|
this.setState({
|
|
|
|
|
results: []
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
clearQuery(){
|
|
|
|
|
this.setState({
|
2019-05-27 11:31:48 -04:00
|
|
|
|
q: ''
|
2019-03-19 11:25:17 -04:00
|
|
|
|
});
|
2019-02-11 04:04:19 -05:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-19 06:35:03 -04:00
|
|
|
|
expandSearch(e){
|
|
|
|
|
this.setState(state => ({
|
|
|
|
|
collapsedSearch: !state.collapsedSearch
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-11 04:04:19 -05:00
|
|
|
|
// Query search endpoint
|
|
|
|
|
search(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this.setState({
|
|
|
|
|
fetching: true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
fetch(
|
2019-08-14 09:05:49 -04:00
|
|
|
|
'/api/search?q='+this.state.q
|
2019-02-11 04:04:19 -05:00
|
|
|
|
).then(
|
|
|
|
|
(res) => res.json()
|
|
|
|
|
).then((data) => {
|
|
|
|
|
if (data && data.results){
|
|
|
|
|
this.setState({
|
|
|
|
|
results: data.results,
|
|
|
|
|
fetching: false
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
console.error(data);
|
|
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
|
results: [],
|
|
|
|
|
fetching: false
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}).catch((err) => {
|
2019-05-27 11:39:16 -04:00
|
|
|
|
console.error(err)
|
2019-02-11 04:04:19 -05:00
|
|
|
|
|
2019-05-27 11:39:16 -04:00
|
|
|
|
this.setState({
|
|
|
|
|
results: [],
|
|
|
|
|
fetching: false
|
|
|
|
|
})
|
2019-02-11 04:04:19 -05:00
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-19 06:35:03 -04:00
|
|
|
|
componentDidMount() {
|
|
|
|
|
window.addEventListener('resize', this.onResize);
|
2019-07-07 14:20:04 -04:00
|
|
|
|
if (window && window.innerHeight) {
|
2019-06-19 06:35:03 -04:00
|
|
|
|
// if we're in the browser, pass in as though from event to initialise
|
|
|
|
|
this.onResize({target: window});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
|
|
|
|
window.removeEventListener('resize', this.onResize);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// On a real mobile device onResize() gets called when the virtual keyboard pops up (e.g. when entering search text)
|
|
|
|
|
// so be careful what states are changed in this method (i.e. don't collapse the search box here)
|
|
|
|
|
onResize(e) {
|
2019-07-07 14:20:04 -04:00
|
|
|
|
this.setState({smallScreen: (e.target.innerWidth < 768)});
|
2019-06-19 06:35:03 -04:00
|
|
|
|
}
|
|
|
|
|
|
2019-02-11 04:04:19 -05:00
|
|
|
|
render() {
|
2019-06-19 06:35:03 -04:00
|
|
|
|
// if the current state is collapsed (and a mobile device) just render the icon
|
|
|
|
|
if(this.state.collapsedSearch && this.state.smallScreen){
|
|
|
|
|
return(
|
|
|
|
|
<div className="collapse-btn" onClick={this.expandSearch}>
|
|
|
|
|
<SearchIcon />
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-11 04:04:19 -05:00
|
|
|
|
const resultsList = this.state.results.length?
|
|
|
|
|
<ul className="search-box-results">
|
2019-05-27 11:39:16 -04:00
|
|
|
|
{
|
|
|
|
|
this.state.results.map((result) => {
|
|
|
|
|
const label = result.attributes.label;
|
|
|
|
|
const lng = result.geometry.coordinates[0];
|
|
|
|
|
const lat = result.geometry.coordinates[1];
|
|
|
|
|
const zoom = result.attributes.zoom;
|
|
|
|
|
const href = `?lng=${lng}&lat=${lat}&zoom=${zoom}`
|
|
|
|
|
return (
|
|
|
|
|
<li key={result.attributes.label}>
|
|
|
|
|
<a
|
|
|
|
|
className="search-box-result"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this.props.onLocate(lat, lng, zoom);
|
|
|
|
|
}}
|
|
|
|
|
href={href}
|
2019-05-27 13:26:29 -04:00
|
|
|
|
>{`${label.substring(0, 4)} ${label.substring(4, 7)}`}</a>
|
2019-05-27 11:39:16 -04:00
|
|
|
|
</li>
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
}
|
2019-02-11 04:04:19 -05:00
|
|
|
|
</ul>
|
2019-05-27 11:39:16 -04:00
|
|
|
|
: null;
|
2019-02-11 04:04:19 -05:00
|
|
|
|
return (
|
2019-05-27 13:26:29 -04:00
|
|
|
|
<div className={`search-box ${this.props.isBuilding? 'building' : ''}`} onKeyDown={this.handleKeyPress}>
|
2019-08-15 07:19:43 -04:00
|
|
|
|
<form onSubmit={this.search} className="form-inline">
|
2019-06-19 06:35:03 -04:00
|
|
|
|
<div onClick={this.state.smallScreen ? this.expandSearch : null}>
|
|
|
|
|
<SearchIcon/>
|
|
|
|
|
</div>
|
2019-02-11 04:04:19 -05:00
|
|
|
|
<input
|
|
|
|
|
className="form-control"
|
|
|
|
|
type="search"
|
|
|
|
|
id="search-box-q"
|
|
|
|
|
name="q"
|
|
|
|
|
value={this.state.q}
|
|
|
|
|
placeholder="Search for a postcode"
|
|
|
|
|
aria-label="Search for a postcode"
|
|
|
|
|
onChange={this.handleChange}
|
2019-05-27 11:39:16 -04:00
|
|
|
|
/>
|
2019-06-19 06:35:03 -04:00
|
|
|
|
<button className="btn btn-outline-dark" type="submit">Search</button>
|
2019-02-11 04:04:19 -05:00
|
|
|
|
</form>
|
|
|
|
|
{ resultsList }
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default SearchBox;
|