2019-11-26 07:09:27 -05:00
|
|
|
import { parse as parseQuery } from 'query-string';
|
2019-09-08 20:09:05 -04:00
|
|
|
import React, { Fragment } from 'react';
|
2019-11-07 02:39:26 -05:00
|
|
|
import { Redirect, Route, RouteComponentProps, Switch } from 'react-router-dom';
|
2019-09-08 20:09:05 -04:00
|
|
|
|
2019-11-26 07:09:27 -05:00
|
|
|
import { parseJsonOrDefault } from '../helpers';
|
2019-11-14 10:25:19 -05:00
|
|
|
import { strictParseInt } from '../parse';
|
|
|
|
|
2019-11-07 02:39:26 -05:00
|
|
|
import BuildingView from './building/building-view';
|
2019-09-08 20:09:05 -04:00
|
|
|
import Categories from './building/categories';
|
2019-11-07 02:39:26 -05:00
|
|
|
import { EditHistory } from './building/edit-history/edit-history';
|
2019-09-08 20:09:05 -04:00
|
|
|
import MultiEdit from './building/multi-edit';
|
2019-11-07 02:39:26 -05:00
|
|
|
import Sidebar from './building/sidebar';
|
2019-09-08 20:09:05 -04:00
|
|
|
import ColouringMap from './map/map';
|
2019-10-24 07:20:48 -04:00
|
|
|
import { Building } from './models/building';
|
2019-11-07 02:39:26 -05:00
|
|
|
import Welcome from './pages/welcome';
|
2019-09-08 20:09:05 -04:00
|
|
|
|
|
|
|
interface MapAppRouteParams {
|
|
|
|
mode: 'view' | 'edit' | 'multi-edit';
|
|
|
|
category: string;
|
|
|
|
building?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface MapAppProps extends RouteComponentProps<MapAppRouteParams> {
|
2019-11-05 15:13:10 -05:00
|
|
|
building?: Building;
|
|
|
|
building_like?: boolean;
|
|
|
|
user?: any;
|
|
|
|
revisionId?: number;
|
2019-09-08 20:09:05 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
interface MapAppState {
|
|
|
|
category: string;
|
|
|
|
revision_id: number;
|
2019-10-24 07:20:48 -04:00
|
|
|
building: Building;
|
2019-09-08 20:09:05 -04:00
|
|
|
building_like: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
class MapApp extends React.Component<MapAppProps, MapAppState> {
|
|
|
|
constructor(props: Readonly<MapAppProps>) {
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
category: this.getCategory(props.match.params.category),
|
2019-10-29 12:56:49 -04:00
|
|
|
revision_id: props.revisionId || 0,
|
2019-09-08 20:09:05 -04:00
|
|
|
building: props.building,
|
|
|
|
building_like: props.building_like
|
|
|
|
};
|
|
|
|
|
|
|
|
this.selectBuilding = this.selectBuilding.bind(this);
|
|
|
|
this.colourBuilding = this.colourBuilding.bind(this);
|
|
|
|
this.increaseRevision = this.increaseRevision.bind(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
componentWillReceiveProps(props: Readonly<MapAppProps>) {
|
|
|
|
const newCategory = this.getCategory(props.match.params.category);
|
|
|
|
if (newCategory != undefined) {
|
|
|
|
this.setState({ category: newCategory });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-29 12:56:49 -04:00
|
|
|
componentDidMount() {
|
|
|
|
this.fetchLatestRevision();
|
2019-11-14 10:25:19 -05:00
|
|
|
this.fetchBuildingData();
|
2019-10-29 12:56:49 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
async fetchLatestRevision() {
|
|
|
|
try {
|
|
|
|
const res = await fetch(`/api/buildings/revision`, {
|
|
|
|
method: 'GET',
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
},
|
|
|
|
credentials: 'same-origin'
|
|
|
|
});
|
|
|
|
const data = await res.json();
|
|
|
|
|
|
|
|
this.increaseRevision(data.latestRevisionId);
|
|
|
|
} catch(error) {
|
|
|
|
console.error(error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-14 10:25:19 -05:00
|
|
|
/**
|
|
|
|
* Fetches building data if a building is selected but no data provided through
|
|
|
|
* props (from server-side rendering)
|
|
|
|
*/
|
|
|
|
async fetchBuildingData() {
|
|
|
|
if(this.props.match.params.building != undefined && this.props.building == undefined) {
|
|
|
|
try {
|
|
|
|
// TODO: simplify API calls, create helpers for fetching data
|
|
|
|
const buildingId = strictParseInt(this.props.match.params.building);
|
|
|
|
let [building, building_uprns, building_like] = await Promise.all([
|
|
|
|
fetch(`/api/buildings/${buildingId}.json`, {
|
|
|
|
method: 'GET',
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
},
|
|
|
|
credentials: 'same-origin'
|
|
|
|
}).then(res => res.json()),
|
|
|
|
fetch(`/api/buildings/${buildingId}/uprns.json`, {
|
|
|
|
method: 'GET',
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
},
|
|
|
|
credentials: 'same-origin'
|
|
|
|
}).then(res => res.json()),
|
|
|
|
fetch(`/api/buildings/${buildingId}/like.json`, {
|
|
|
|
method: 'GET',
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
},
|
|
|
|
credentials: 'same-origin'
|
|
|
|
}).then(res => res.json())
|
|
|
|
]);
|
|
|
|
|
|
|
|
building.uprns = building_uprns.uprns;
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
building: building,
|
|
|
|
building_like: building_like.like
|
|
|
|
});
|
|
|
|
} catch(error) {
|
|
|
|
console.error(error);
|
|
|
|
// TODO: add UI for API errors
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-08 20:09:05 -04:00
|
|
|
getCategory(category: string) {
|
|
|
|
if (category === 'categories') return undefined;
|
|
|
|
|
|
|
|
return category;
|
|
|
|
}
|
|
|
|
|
2019-11-26 07:09:27 -05:00
|
|
|
getMultiEditDataString(): string {
|
|
|
|
const q = parseQuery(this.props.location.search);
|
|
|
|
if(Array.isArray(q.data)) {
|
|
|
|
throw new Error('Invalid format');
|
|
|
|
} else return q.data;
|
|
|
|
}
|
|
|
|
|
2019-09-08 20:09:05 -04:00
|
|
|
increaseRevision(revisionId) {
|
|
|
|
revisionId = +revisionId;
|
|
|
|
// bump revision id, only ever increasing
|
|
|
|
if (revisionId > this.state.revision_id) {
|
2019-11-07 03:13:30 -05:00
|
|
|
this.setState({ revision_id: revisionId });
|
2019-09-08 20:09:05 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-17 12:38:44 -04:00
|
|
|
selectBuilding(building: Building) {
|
2019-09-08 20:09:05 -04:00
|
|
|
const mode = this.props.match.params.mode || 'view';
|
|
|
|
const category = this.props.match.params.category || 'age';
|
|
|
|
|
2019-10-02 11:47:45 -04:00
|
|
|
if (building == undefined) {
|
2019-09-08 20:09:05 -04:00
|
|
|
this.setState({ building: undefined });
|
|
|
|
this.props.history.push(`/${mode}/${category}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.increaseRevision(building.revision_id);
|
|
|
|
// get UPRNs and update
|
|
|
|
fetch(`/api/buildings/${building.building_id}/uprns.json`, {
|
|
|
|
method: 'GET',
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
},
|
|
|
|
credentials: 'same-origin'
|
|
|
|
}).then(
|
|
|
|
res => res.json()
|
|
|
|
).then((res) => {
|
|
|
|
if (res.error) {
|
|
|
|
console.error(res);
|
|
|
|
} else {
|
|
|
|
building.uprns = res.uprns;
|
|
|
|
this.setState({ building: building });
|
|
|
|
}
|
|
|
|
}).catch((err) => {
|
2019-11-07 03:13:30 -05:00
|
|
|
console.error(err);
|
2019-09-08 20:09:05 -04:00
|
|
|
this.setState({ building: building });
|
|
|
|
});
|
|
|
|
|
|
|
|
// get if liked and update
|
|
|
|
fetch(`/api/buildings/${building.building_id}/like.json`, {
|
|
|
|
method: 'GET',
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
},
|
|
|
|
credentials: 'same-origin'
|
|
|
|
}).then(
|
|
|
|
res => res.json()
|
|
|
|
).then((res) => {
|
|
|
|
if (res.error) {
|
|
|
|
console.error(res);
|
|
|
|
} else {
|
|
|
|
this.setState({ building_like: res.like });
|
|
|
|
this.props.history.push(`/${mode}/${category}/${building.building_id}`);
|
|
|
|
}
|
|
|
|
}).catch((err) => {
|
2019-11-07 03:13:30 -05:00
|
|
|
console.error(err);
|
2019-09-08 20:09:05 -04:00
|
|
|
this.setState({ building_like: false });
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Colour building
|
|
|
|
*
|
|
|
|
* Used in multi-edit mode to colour buildings on map click
|
|
|
|
*
|
|
|
|
* Pulls data from URL to form update
|
|
|
|
*
|
2019-11-26 07:09:27 -05:00
|
|
|
* @param {Building} building
|
2019-09-08 20:09:05 -04:00
|
|
|
*/
|
2019-11-26 07:09:27 -05:00
|
|
|
colourBuilding(building: Building) {
|
2019-09-08 20:09:05 -04:00
|
|
|
const cat = this.props.match.params.category;
|
2019-11-26 07:09:27 -05:00
|
|
|
|
2019-09-08 20:09:05 -04:00
|
|
|
if (cat === 'like') {
|
2019-11-07 03:13:30 -05:00
|
|
|
this.likeBuilding(building.building_id);
|
2019-09-08 20:09:05 -04:00
|
|
|
} else {
|
2019-11-26 07:09:27 -05:00
|
|
|
const data = parseJsonOrDefault(this.getMultiEditDataString());
|
|
|
|
|
|
|
|
|
|
|
|
if (data != undefined && !Object.values(data).some(x => x == undefined)) {
|
2019-11-07 03:13:30 -05:00
|
|
|
this.updateBuilding(building.building_id, data);
|
2019-10-02 17:13:34 -04:00
|
|
|
}
|
2019-09-08 20:09:05 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
likeBuilding(buildingId) {
|
|
|
|
fetch(`/api/buildings/${buildingId}/like.json`, {
|
|
|
|
method: 'POST',
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
},
|
|
|
|
credentials: 'same-origin',
|
|
|
|
body: JSON.stringify({ like: true })
|
|
|
|
}).then(
|
|
|
|
res => res.json()
|
|
|
|
).then(function (res) {
|
|
|
|
if (res.error) {
|
2019-11-07 03:13:30 -05:00
|
|
|
console.error({ error: res.error });
|
2019-09-08 20:09:05 -04:00
|
|
|
} else {
|
|
|
|
this.increaseRevision(res.revision_id);
|
|
|
|
}
|
|
|
|
}.bind(this)).catch(
|
|
|
|
(err) => console.error({ error: err })
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
updateBuilding(buildingId, data) {
|
|
|
|
fetch(`/api/buildings/${buildingId}.json`, {
|
|
|
|
method: 'POST',
|
|
|
|
body: JSON.stringify(data),
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
},
|
|
|
|
credentials: 'same-origin'
|
|
|
|
}).then(
|
|
|
|
res => res.json()
|
|
|
|
).then(res => {
|
|
|
|
if (res.error) {
|
2019-11-07 03:13:30 -05:00
|
|
|
console.error({ error: res.error });
|
2019-09-08 20:09:05 -04:00
|
|
|
} else {
|
|
|
|
this.increaseRevision(res.revision_id);
|
|
|
|
}
|
|
|
|
}).catch(
|
|
|
|
(err) => console.error({ error: err })
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
2019-10-15 09:37:23 -04:00
|
|
|
const mode = this.props.match.params.mode;
|
2019-10-30 08:28:10 -04:00
|
|
|
const viewEditMode = mode === 'multi-edit' ? undefined : mode;
|
2019-09-08 20:09:05 -04:00
|
|
|
|
|
|
|
let category = this.state.category || 'age';
|
|
|
|
|
|
|
|
const building_id = this.state.building && this.state.building.building_id;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Fragment>
|
|
|
|
<Switch>
|
|
|
|
<Route exact path="/">
|
|
|
|
<Welcome />
|
|
|
|
</Route>
|
|
|
|
<Route exact path="/:mode/categories/:building?">
|
|
|
|
<Sidebar>
|
2019-11-05 15:13:10 -05:00
|
|
|
<Categories mode={mode || 'view'} building_id={building_id} />
|
2019-09-08 20:09:05 -04:00
|
|
|
</Sidebar>
|
|
|
|
</Route>
|
|
|
|
<Route exact path="/multi-edit/:cat" render={(props) => (
|
|
|
|
<MultiEdit
|
2019-11-26 07:09:27 -05:00
|
|
|
category={category}
|
|
|
|
dataString={this.getMultiEditDataString()}
|
2019-09-08 20:09:05 -04:00
|
|
|
user={this.props.user}
|
|
|
|
/>
|
|
|
|
)} />
|
|
|
|
<Route exact path="/:mode/:cat/:building?">
|
|
|
|
<Sidebar>
|
|
|
|
<BuildingView
|
2019-10-30 08:28:10 -04:00
|
|
|
mode={viewEditMode}
|
2019-09-08 20:09:05 -04:00
|
|
|
cat={category}
|
|
|
|
building={this.state.building}
|
|
|
|
building_like={this.state.building_like}
|
|
|
|
selectBuilding={this.selectBuilding}
|
|
|
|
user={this.props.user}
|
|
|
|
/>
|
|
|
|
</Sidebar>
|
|
|
|
</Route>
|
2019-10-24 07:20:48 -04:00
|
|
|
<Route exact path="/:mode/:cat/:building/history">
|
|
|
|
<Sidebar>
|
|
|
|
<EditHistory building={this.state.building} />
|
|
|
|
</Sidebar>
|
|
|
|
</Route>
|
2019-10-15 09:53:01 -04:00
|
|
|
<Route exact path="/:mode(view|edit|multi-edit)"
|
|
|
|
render={props => (<Redirect to={`/${props.match.params.mode}/categories`} />)}
|
|
|
|
/>
|
2019-09-08 20:09:05 -04:00
|
|
|
</Switch>
|
|
|
|
<ColouringMap
|
|
|
|
building={this.state.building}
|
2019-10-15 09:37:23 -04:00
|
|
|
mode={mode || 'basic'}
|
2019-09-08 20:09:05 -04:00
|
|
|
category={category}
|
|
|
|
revision_id={this.state.revision_id}
|
|
|
|
selectBuilding={this.selectBuilding}
|
|
|
|
colourBuilding={this.colourBuilding}
|
|
|
|
/>
|
|
|
|
</Fragment>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-02 17:13:34 -04:00
|
|
|
export default MapApp;
|