2018-09-09 17:22:44 -04:00
|
|
|
import React, { Fragment } from 'react';
|
2018-09-13 15:41:42 -04:00
|
|
|
import { Route, Switch, Link } from 'react-router-dom';
|
2019-05-27 13:26:29 -04:00
|
|
|
import PropTypes from 'prop-types';
|
2019-05-10 11:10:16 -04:00
|
|
|
import { parse } from 'query-string';
|
2018-09-09 17:22:44 -04:00
|
|
|
|
2018-09-17 16:27:52 -04:00
|
|
|
import '../../node_modules/bootstrap/dist/css/bootstrap.min.css';
|
2018-10-20 06:33:27 -04:00
|
|
|
import './app.css';
|
2018-09-17 16:27:52 -04:00
|
|
|
|
2018-09-09 17:22:44 -04:00
|
|
|
import AboutPage from './about';
|
2018-09-10 18:34:56 -04:00
|
|
|
import BuildingEdit from './building-edit';
|
|
|
|
import BuildingView from './building-view';
|
2019-05-09 04:16:36 -04:00
|
|
|
import MultiEdit from './multi-edit';
|
2018-09-10 18:34:56 -04:00
|
|
|
import ColouringMap from './map';
|
2018-09-09 17:22:44 -04:00
|
|
|
import Header from './header';
|
2018-11-29 17:00:53 -05:00
|
|
|
import Overview from './overview';
|
2018-09-09 17:22:44 -04:00
|
|
|
import Login from './login';
|
2018-09-10 18:34:56 -04:00
|
|
|
import MyAccountPage from './my-account';
|
2018-09-09 17:22:44 -04:00
|
|
|
import SignUp from './signup';
|
|
|
|
import Welcome from './welcome';
|
2019-05-10 11:10:16 -04:00
|
|
|
import { parseCategoryURL } from '../parse';
|
2019-08-13 16:20:20 -04:00
|
|
|
import PrivacyPolicyPage from './privacy-policy';
|
2019-08-13 16:17:39 -04:00
|
|
|
import ContributorAgreementPage from './contributor-agreement';
|
2018-09-09 17:22:44 -04:00
|
|
|
|
2019-02-05 16:41:31 -05:00
|
|
|
/**
|
|
|
|
* App component
|
|
|
|
*
|
|
|
|
* This is the top-level stateful frontend component
|
|
|
|
* - rendered from props, instantiated either server-side in server.js or client-side in
|
|
|
|
* client.js
|
|
|
|
* - state (including user, current building) is initialised from props
|
|
|
|
* - callbacks to update top-level state are passed down to subcomponents
|
|
|
|
* - render method wraps a react-router switch - this drives which version of the sidebar and
|
|
|
|
* map or other pages are rendered, based on the URL. Use a react-router-dom <Link /> in
|
|
|
|
* child components to navigate without a full page reload.
|
|
|
|
*/
|
2019-08-09 13:49:43 -04:00
|
|
|
class App extends React.Component<any, any> { // TODO: add proper types
|
|
|
|
static propTypes = { // TODO: generate propTypes from TS
|
|
|
|
user: PropTypes.object,
|
|
|
|
building: PropTypes.object,
|
|
|
|
building_like: PropTypes.bool
|
|
|
|
}
|
|
|
|
|
2018-09-09 17:22:44 -04:00
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
2019-05-09 04:16:36 -04:00
|
|
|
// set building revision id, default 0
|
2019-05-27 15:22:41 -04:00
|
|
|
const rev = (props.building)? +props.building.revision_id : 0;
|
2018-09-10 18:34:56 -04:00
|
|
|
this.state = {
|
|
|
|
user: props.user,
|
|
|
|
building: props.building,
|
2019-05-09 04:16:36 -04:00
|
|
|
building_like: props.building_like,
|
|
|
|
revision_id: rev
|
2018-09-10 18:34:56 -04:00
|
|
|
};
|
2018-09-09 17:22:44 -04:00
|
|
|
this.login = this.login.bind(this);
|
2018-10-20 07:20:10 -04:00
|
|
|
this.updateUser = this.updateUser.bind(this);
|
2018-09-09 17:22:44 -04:00
|
|
|
this.logout = this.logout.bind(this);
|
2018-09-10 18:34:56 -04:00
|
|
|
this.selectBuilding = this.selectBuilding.bind(this);
|
2019-05-09 04:16:36 -04:00
|
|
|
this.colourBuilding = this.colourBuilding.bind(this);
|
|
|
|
this.increaseRevision = this.increaseRevision.bind(this);
|
2018-09-09 17:22:44 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
login(user) {
|
2018-09-30 15:28:33 -04:00
|
|
|
if (user.error) {
|
|
|
|
this.logout();
|
|
|
|
return
|
|
|
|
}
|
2018-09-09 17:22:44 -04:00
|
|
|
this.setState({user: user});
|
|
|
|
}
|
|
|
|
|
2018-10-20 07:20:10 -04:00
|
|
|
updateUser(user){
|
|
|
|
this.setState({user: { ...this.state.user, ...user }});
|
|
|
|
}
|
|
|
|
|
2018-09-10 18:34:56 -04:00
|
|
|
logout() {
|
2018-09-09 17:22:44 -04:00
|
|
|
this.setState({user: undefined});
|
|
|
|
}
|
|
|
|
|
2019-05-27 15:13:43 -04:00
|
|
|
increaseRevision(revisionId) {
|
2019-05-27 15:22:41 -04:00
|
|
|
revisionId = +revisionId;
|
2019-05-09 04:16:36 -04:00
|
|
|
// bump revision id, only ever increasing
|
2019-05-27 15:13:43 -04:00
|
|
|
if (revisionId > this.state.revision_id){
|
|
|
|
this.setState({revision_id: revisionId})
|
2019-05-09 04:16:36 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-10 18:34:56 -04:00
|
|
|
selectBuilding(building) {
|
2019-05-09 04:16:36 -04:00
|
|
|
this.increaseRevision(building.revision_id);
|
|
|
|
// get UPRNs and update
|
2019-08-14 09:05:49 -04:00
|
|
|
fetch(`/api/buildings/${building.building_id}/uprns.json`, {
|
2018-10-25 09:36:52 -04:00
|
|
|
method: 'GET',
|
|
|
|
headers:{
|
2019-05-27 11:39:16 -04:00
|
|
|
'Content-Type': 'application/json'
|
2018-10-25 09:36:52 -04:00
|
|
|
},
|
|
|
|
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});
|
|
|
|
});
|
2019-01-22 12:21:44 -05:00
|
|
|
|
|
|
|
// get if liked and update
|
2019-08-14 09:05:49 -04:00
|
|
|
fetch(`/api/buildings/${building.building_id}/like.json`, {
|
2019-01-22 12:21:44 -05:00
|
|
|
method: 'GET',
|
|
|
|
headers:{
|
2019-05-27 11:39:16 -04:00
|
|
|
'Content-Type': 'application/json'
|
2019-01-22 12:21:44 -05:00
|
|
|
},
|
|
|
|
credentials: 'same-origin'
|
|
|
|
}).then(
|
|
|
|
res => res.json()
|
|
|
|
).then((res) => {
|
|
|
|
if (res.error) {
|
|
|
|
console.error(res);
|
|
|
|
} else {
|
|
|
|
this.setState({building_like: res.like});
|
|
|
|
}
|
|
|
|
}).catch((err) => {
|
|
|
|
console.error(err)
|
|
|
|
this.setState({building_like: false});
|
|
|
|
});
|
2018-09-10 18:34:56 -04:00
|
|
|
}
|
|
|
|
|
2019-08-01 05:29:32 -04:00
|
|
|
/**
|
|
|
|
* Colour building
|
|
|
|
*
|
|
|
|
* Used in multi-edit mode to colour buildings on map click
|
|
|
|
*
|
|
|
|
* Pulls data from URL to form update
|
|
|
|
*
|
|
|
|
* @param {object} building
|
|
|
|
*/
|
2019-05-09 04:16:36 -04:00
|
|
|
colourBuilding(building) {
|
2019-05-10 11:10:16 -04:00
|
|
|
const cat = parseCategoryURL(window.location.pathname);
|
|
|
|
const q = parse(window.location.search);
|
2019-08-09 13:49:43 -04:00
|
|
|
const data = (cat === 'like')? {like: true}: JSON.parse(q.data as string); // TODO: verify what happens if data is string[]
|
2019-05-10 11:10:16 -04:00
|
|
|
if (cat === 'like'){
|
|
|
|
this.likeBuilding(building.building_id)
|
|
|
|
} else {
|
|
|
|
this.updateBuilding(building.building_id, data)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-27 15:13:43 -04:00
|
|
|
likeBuilding(buildingId) {
|
2019-08-14 09:05:49 -04:00
|
|
|
fetch(`/api/buildings/${buildingId}/like.json`, {
|
2019-05-10 11:10:16 -04:00
|
|
|
method: 'POST',
|
|
|
|
headers:{
|
2019-05-27 15:13:43 -04:00
|
|
|
'Content-Type': 'application/json'
|
2019-05-10 11:10:16 -04:00
|
|
|
},
|
|
|
|
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})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-05-27 15:13:43 -04:00
|
|
|
updateBuilding(buildingId, data){
|
2019-08-14 09:05:49 -04:00
|
|
|
fetch(`/api/buildings/${buildingId}.json`, {
|
2019-05-09 04:16:36 -04:00
|
|
|
method: 'POST',
|
2019-05-10 11:10:16 -04:00
|
|
|
body: JSON.stringify(data),
|
2019-05-09 04:16:36 -04:00
|
|
|
headers:{
|
2019-05-27 15:13:43 -04:00
|
|
|
'Content-Type': 'application/json'
|
2019-05-09 04:16:36 -04:00
|
|
|
},
|
|
|
|
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})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-09-09 17:22:44 -04:00
|
|
|
render() {
|
|
|
|
return (
|
|
|
|
<Fragment>
|
|
|
|
<Header user={this.state.user} />
|
2018-11-13 04:24:36 -05:00
|
|
|
<main>
|
2019-01-22 13:39:14 -05:00
|
|
|
<Switch>
|
|
|
|
<Route exact path="/">
|
|
|
|
<Welcome />
|
|
|
|
</Route>
|
|
|
|
<Route exact path="/view/:cat.html" render={(props) => (
|
|
|
|
<Overview
|
|
|
|
{...props}
|
|
|
|
mode='view' user={this.state.user}
|
2019-05-27 11:39:16 -04:00
|
|
|
/>
|
2019-01-22 13:39:14 -05:00
|
|
|
) } />
|
|
|
|
<Route exact path="/edit/:cat.html" render={(props) => (
|
|
|
|
<Overview
|
|
|
|
{...props}
|
|
|
|
mode='edit' user={this.state.user}
|
2019-05-27 11:39:16 -04:00
|
|
|
/>
|
2019-01-22 13:39:14 -05:00
|
|
|
) } />
|
2019-05-09 04:16:36 -04:00
|
|
|
<Route exact path="/multi-edit/:cat.html" render={(props) => (
|
|
|
|
<MultiEdit
|
|
|
|
{...props}
|
|
|
|
user={this.state.user}
|
2019-05-27 15:13:43 -04:00
|
|
|
/>
|
2019-05-09 04:16:36 -04:00
|
|
|
) } />
|
2019-01-22 13:39:14 -05:00
|
|
|
<Route exact path="/view/:cat/building/:building.html" render={(props) => (
|
|
|
|
<BuildingView
|
|
|
|
{...props}
|
|
|
|
{...this.state.building}
|
|
|
|
user={this.state.user}
|
|
|
|
building_like={this.state.building_like}
|
2019-05-27 11:39:16 -04:00
|
|
|
/>
|
2019-01-22 13:39:14 -05:00
|
|
|
) } />
|
|
|
|
<Route exact path="/edit/:cat/building/:building.html" render={(props) => (
|
|
|
|
<BuildingEdit
|
|
|
|
{...props}
|
|
|
|
{...this.state.building}
|
|
|
|
user={this.state.user}
|
|
|
|
building_like={this.state.building_like}
|
|
|
|
selectBuilding={this.selectBuilding}
|
2019-05-27 11:39:16 -04:00
|
|
|
/>
|
2019-01-22 13:39:14 -05:00
|
|
|
) } />
|
|
|
|
</Switch>
|
2018-09-10 07:41:00 -04:00
|
|
|
<Switch>
|
2019-05-09 04:16:36 -04:00
|
|
|
<Route exact path="/(multi-edit.*|edit.*|view.*)?" render={(props) => (
|
2018-10-03 16:46:51 -04:00
|
|
|
<ColouringMap
|
|
|
|
{...props}
|
|
|
|
building={this.state.building}
|
2019-05-09 04:16:36 -04:00
|
|
|
revision_id={this.state.revision_id}
|
2018-10-03 16:46:51 -04:00
|
|
|
selectBuilding={this.selectBuilding}
|
2019-05-09 04:16:36 -04:00
|
|
|
colourBuilding={this.colourBuilding}
|
2019-05-27 11:39:16 -04:00
|
|
|
/>
|
2018-09-10 17:14:09 -04:00
|
|
|
) } />
|
2018-09-10 07:41:00 -04:00
|
|
|
<Route exact path="/about.html" component={AboutPage} />
|
|
|
|
<Route exact path="/login.html">
|
|
|
|
<Login user={this.state.user} login={this.login} />
|
|
|
|
</Route>
|
|
|
|
<Route exact path="/sign-up.html">
|
|
|
|
<SignUp user={this.state.user} login={this.login} />
|
|
|
|
</Route>
|
|
|
|
<Route exact path="/my-account.html">
|
2018-10-20 07:20:10 -04:00
|
|
|
<MyAccountPage
|
|
|
|
user={this.state.user}
|
|
|
|
updateUser={this.updateUser}
|
|
|
|
logout={this.logout}
|
2019-05-27 11:39:16 -04:00
|
|
|
/>
|
2018-09-10 07:41:00 -04:00
|
|
|
</Route>
|
2019-08-13 16:20:20 -04:00
|
|
|
<Route exact path="/privacy-policy.html" component={PrivacyPolicyPage} />
|
2019-08-13 16:17:39 -04:00
|
|
|
<Route exact path="/contributor-agreement.html" component={ContributorAgreementPage} />
|
2018-09-10 07:41:00 -04:00
|
|
|
<Route component={NotFound} />
|
|
|
|
</Switch>
|
2018-09-09 17:22:44 -04:00
|
|
|
</main>
|
|
|
|
</Fragment>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2019-02-05 16:41:31 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Component to fall back on in case of 404 or no other match
|
|
|
|
*/
|
2018-09-09 17:22:44 -04:00
|
|
|
const NotFound = () => (
|
|
|
|
<article>
|
|
|
|
<section className="main-col">
|
2018-10-03 16:46:51 -04:00
|
|
|
<h1 className="h1">Page not found</h1>
|
2018-09-13 15:41:42 -04:00
|
|
|
<p className="lead">
|
|
|
|
|
2019-05-27 11:23:58 -04:00
|
|
|
We can’t find that one anywhere.
|
2018-09-13 15:41:42 -04:00
|
|
|
|
|
|
|
</p>
|
|
|
|
<Link className="btn btn-outline-dark" to="/">Back home</Link>
|
2018-09-09 17:22:44 -04:00
|
|
|
</section>
|
|
|
|
</article>
|
|
|
|
);
|
|
|
|
|
|
|
|
export default App;
|