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

268 lines
9.0 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';
2020-01-02 05:59:13 -05:00
import { apiGet, apiPost } from './apiHelpers';
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';
import { PrivateRoute } from './route';
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;
user_verified?: object;
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;
user_verified: object;
2019-09-08 20:09:05 -04:00
}
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,
user_verified: props.user_verified || {}
2019-09-08 20:09:05 -04:00
};
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();
if(this.props.match.params.building != undefined && this.props.building == undefined) {
this.fetchBuildingData(strictParseInt(this.props.match.params.building));
}
}
async fetchLatestRevision() {
try {
2020-01-02 05:59:13 -05:00
const {latestRevisionId} = await apiGet(`/api/buildings/revision`);
2020-02-03 17:35:32 -05:00
2020-01-02 05:59:13 -05:00
this.increaseRevision(latestRevisionId);
} catch(error) {
console.error(error);
}
}
/**
2020-02-03 17:35:32 -05:00
* Fetches building data if a building is selected but no data provided through
* props (from server-side rendering)
*/
async fetchBuildingData(buildingId: number) {
try {
// TODO: simplify API calls, create helpers for fetching data
let [building, building_uprns, building_like, user_verified] = await Promise.all([
apiGet(`/api/buildings/${buildingId}.json`),
apiGet(`/api/buildings/${buildingId}/uprns.json`),
apiGet(`/api/buildings/${buildingId}/like.json`),
apiGet(`/api/buildings/${buildingId}/verify.json`)
]);
building.uprns = building_uprns.uprns;
this.setState({
building: building,
building_like: building_like.like,
user_verified: user_verified
});
this.increaseRevision(building.revision_id);
} 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.fetchBuildingData(building.building_id);
this.props.history.push(`/${mode}/${category}/${building.building_id}`);
2019-09-08 20:09:05 -04:00
}
/**
* 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;
2020-02-03 17:35:32 -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());
2020-02-03 17:35:32 -05:00
2019-11-26 07:09:27 -05:00
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) {
2020-01-02 05:59:13 -05:00
apiPost(`/api/buildings/${buildingId}/like.json`, { like: true })
.then(res => {
2019-09-08 20:09:05 -04:00
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);
}
2020-01-02 05:59:13 -05:00
}).catch(
2019-09-08 20:09:05 -04:00
(err) => console.error({ error: err })
);
}
updateBuilding(buildingId, data) {
2020-01-02 05:59:13 -05:00
apiPost(`/api/buildings/${buildingId}.json`, data)
.then(res => {
2019-09-08 20:09:05 -04:00
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>
<PrivateRoute path="/:mode(edit|multi-edit)" /> {/* empty private route to ensure auth for editing */}
</Switch>
2019-09-08 20:09:05 -04:00
<Switch>
<Route exact path="/">
2020-04-09 10:57:17 -04:00
<Sidebar>
<Welcome />
</Sidebar>
2019-09-08 20:09:05 -04:00
</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>
2020-02-03 17:35:32 -05:00
<Categories mode={mode || 'view'} building_id={building_id} />
2019-09-08 20:09:05 -04:00
<BuildingView
mode={viewEditMode}
2019-09-08 20:09:05 -04:00
cat={category}
building={this.state.building}
building_like={this.state.building_like}
user_verified={this.state.user_verified}
2019-09-08 20:09:05 -04:00
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>
2020-02-03 17:35:32 -05:00
<Categories mode={mode || 'view'} building_id={building_id} />
2019-10-24 07:20:48 -04:00
<EditHistory building={this.state.building} />
</Sidebar>
</Route>
2019-10-15 09:53:01 -04:00
<Route exact path="/:mode(view|edit|multi-edit)"
2020-02-03 17:35:32 -05:00
render={props => (<Redirect to={`/${props.match.params.mode}/categories`} />)}
2019-10-15 09:53:01 -04:00
/>
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;