colouring-montreal/app/src/frontend/map-app.tsx

330 lines
11 KiB
TypeScript
Raw Normal View History

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';
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> {
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),
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 });
}
}
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
}
}
}
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';
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-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() {
const mode = this.props.match.params.mode;
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>
<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
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}
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>
);
}
}
export default MapApp;