colouring-montreal/app/src/frontend/map-app.tsx
2019-11-14 15:25:19 +00:00

323 lines
11 KiB
TypeScript

import { parse } from 'query-string';
import React, { Fragment } from 'react';
import { Redirect, Route, RouteComponentProps, Switch } from 'react-router-dom';
import { strictParseInt } from '../parse';
import BuildingView from './building/building-view';
import Categories from './building/categories';
import { EditHistory } from './building/edit-history/edit-history';
import MultiEdit from './building/multi-edit';
import Sidebar from './building/sidebar';
import ColouringMap from './map/map';
import { Building } from './models/building';
import Welcome from './pages/welcome';
interface MapAppRouteParams {
mode: 'view' | 'edit' | 'multi-edit';
category: string;
building?: string;
}
interface MapAppProps extends RouteComponentProps<MapAppRouteParams> {
building?: Building;
building_like?: boolean;
user?: any;
revisionId?: number;
}
interface MapAppState {
category: string;
revision_id: number;
building: Building;
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),
revision_id: props.revisionId || 0,
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 });
}
}
componentDidMount() {
this.fetchLatestRevision();
this.fetchBuildingData();
}
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);
}
}
/**
* 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
}
}
}
getCategory(category: string) {
if (category === 'categories') return undefined;
return category;
}
increaseRevision(revisionId) {
revisionId = +revisionId;
// bump revision id, only ever increasing
if (revisionId > this.state.revision_id) {
this.setState({ revision_id: revisionId });
}
}
selectBuilding(building: Building) {
const mode = this.props.match.params.mode || 'view';
const category = this.props.match.params.category || 'age';
if (building == undefined) {
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) => {
console.error(err);
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) => {
console.error(err);
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
*
* @param {object} building
*/
colourBuilding(building) {
const cat = this.props.match.params.category;
const q = parse(window.location.search);
if (cat === 'like') {
this.likeBuilding(building.building_id);
} else {
try {
// TODO: verify what happens if data is string[]
const data = JSON.parse(q.data as string);
this.updateBuilding(building.building_id, data);
} catch (error) {
console.error(error, q);
}
}
}
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) {
console.error({ error: res.error });
} 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) {
console.error({ error: res.error });
} else {
this.increaseRevision(res.revision_id);
}
}).catch(
(err) => console.error({ error: err })
);
}
render() {
const mode = this.props.match.params.mode;
const viewEditMode = mode === 'multi-edit' ? undefined : mode;
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>
<Categories mode={mode || 'view'} building_id={building_id} />
</Sidebar>
</Route>
<Route exact path="/multi-edit/:cat" render={(props) => (
<MultiEdit
{...props}
user={this.props.user}
/>
)} />
<Route exact path="/:mode/:cat/:building?">
<Sidebar>
<BuildingView
mode={viewEditMode}
cat={category}
building={this.state.building}
building_like={this.state.building_like}
selectBuilding={this.selectBuilding}
user={this.props.user}
/>
</Sidebar>
</Route>
<Route exact path="/:mode/:cat/:building/history">
<Sidebar>
<EditHistory building={this.state.building} />
</Sidebar>
</Route>
<Route exact path="/:mode(view|edit|multi-edit)"
render={props => (<Redirect to={`/${props.match.params.mode}/categories`} />)}
/>
</Switch>
<ColouringMap
building={this.state.building}
mode={mode || 'basic'}
category={category}
revision_id={this.state.revision_id}
selectBuilding={this.selectBuilding}
colourBuilding={this.colourBuilding}
/>
</Fragment>
);
}
}
export default MapApp;