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

299 lines
11 KiB
TypeScript
Raw Normal View History

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';
import { parse } from 'query-string';
import '../../node_modules/bootstrap/dist/css/bootstrap.min.css';
import './app.css';
import BuildingEdit from './building/building-edit';
import BuildingView from './building/building-view';
import ColouringMap from './map/map';
import Header from './header';
import MultiEdit from './building/multi-edit';
2019-08-14 06:37:09 -04:00
import Categories from './building/categories';
import AboutPage from './pages/about';
import ContributorAgreementPage from './pages/contributor-agreement';
import PrivacyPolicyPage from './pages/privacy-policy';
import Welcome from './pages/welcome';
import Login from './user/login';
import MyAccountPage from './user/my-account';
import SignUp from './user/signup';
import ForgottenPassword from './forgotten-password';
import PasswordReset from './password-reset';
import { parseCategoryURL } from '../parse';
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.
*/
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
}
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
};
this.login = this.login.bind(this);
2018-10-20 07:20:10 -04:00
this.updateUser = this.updateUser.bind(this);
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);
}
login(user) {
2018-09-30 15:28:33 -04:00
if (user.error) {
this.logout();
return
}
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() {
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`, {
method: 'GET',
headers:{
2019-05-27 11:39:16 -04:00
'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
2019-08-14 09:05:49 -04:00
fetch(`/api/buildings/${building.building_id}/like.json`, {
method: 'GET',
headers:{
2019-05-27 11:39:16 -04:00
'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});
}
}).catch((err) => {
console.error(err)
this.setState({building_like: false});
});
2018-09-10 18:34:56 -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) {
const cat = parseCategoryURL(window.location.pathname);
const q = parse(window.location.search);
const data = (cat === 'like')? {like: true}: JSON.parse(q.data as string); // TODO: verify what happens if data is string[]
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`, {
method: 'POST',
headers:{
2019-05-27 15:13:43 -04:00
'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})
);
}
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',
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})
);
}
render() {
2019-08-14 06:37:09 -04:00
const building_id = (this.state.building)?
this.state.building.building_id
: 2503371 // Default to UCL main building. TODO use last selected if any
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>
2019-08-14 06:37:09 -04:00
<Route exact path="/view/categories.html">
<Categories
mode="view"
building_id={building_id}
2019-05-27 11:39:16 -04:00
/>
2019-08-14 06:37:09 -04:00
</Route>
<Route exact path="/edit/categories.html">
<Categories
mode="edit"
building_id={building_id}
2019-05-27 11:39:16 -04:00
/>
2019-08-14 06:37:09 -04:00
</Route>
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>
2019-08-22 09:40:38 -04:00
<Route exact path="/forgotten-password.html" component={ForgottenPassword} />
<Route exact path="/password-reset.html" component={PasswordReset} />
2018-09-10 07:41:00 -04:00
<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>
<Route exact path="/privacy-policy.html" component={PrivacyPolicyPage} />
<Route exact path="/contributor-agreement.html" component={ContributorAgreementPage} />
2018-09-10 07:41:00 -04:00
<Route component={NotFound} />
</Switch>
</main>
</Fragment>
);
}
}
2019-02-05 16:41:31 -05:00
/**
* Component to fall back on in case of 404 or no other match
*/
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&rsquo;t find that one anywhere.
2018-09-13 15:41:42 -04:00
</p>
<Link className="btn btn-outline-dark" to="/">Back home</Link>
</section>
</article>
);
export default App;