colouring-montreal/app/src/frontend/building-edit.js

559 lines
20 KiB
JavaScript
Raw Normal View History

import React, { Component, Fragment } from 'react';
import { Link, NavLink, Redirect } from 'react-router-dom';
2019-05-27 13:26:29 -04:00
import PropTypes from 'prop-types';
2018-09-13 12:13:03 -04:00
import ErrorBox from './error-box';
2018-09-13 15:41:42 -04:00
import InfoBox from './info-box';
2018-09-11 15:59:59 -04:00
import Sidebar from './sidebar';
2018-10-05 13:41:12 -04:00
import Tooltip from './tooltip';
2018-11-30 04:44:19 -05:00
import { SaveIcon } from './icons';
2018-11-30 04:26:28 -05:00
import { parseCategoryURL } from '../parse';
import CONFIG from './fields-config.json';
2018-10-04 17:50:33 -04:00
const BuildingEdit = (props) => {
if (!props.user){
return <Redirect to="/sign-up.html" />
}
2018-11-30 04:44:19 -05:00
const cat = parseCategoryURL(props.match.url);
2018-10-04 17:50:33 -04:00
if (!props.building_id){
return (
2018-11-30 04:44:19 -05:00
<Sidebar title="Building Not Found" back={`/edit/${cat}.html`}>
2018-10-04 17:50:33 -04:00
<InfoBox msg="We can't find that one anywhere - try the map again?" />
<div className="buttons-container">
2018-11-30 04:44:19 -05:00
<Link to={`/edit/${cat}.html`} className="btn btn-secondary">Back to maps</Link>
2018-10-04 17:50:33 -04:00
</div>
</Sidebar>
);
}
2018-10-04 17:50:33 -04:00
return (
2018-11-29 17:00:53 -05:00
<Sidebar
key={props.building_id}
2019-05-27 11:31:48 -04:00
title={'You are editing'}
2018-11-29 17:00:53 -05:00
back={`/edit/${cat}.html`}>
2018-10-04 17:50:33 -04:00
{
2019-05-27 13:26:29 -04:00
CONFIG.map((section) => {
2018-10-04 17:50:33 -04:00
return <EditForm
2019-05-27 13:26:29 -04:00
{...section} {...props}
cat={cat} key={section.slug} />
2018-10-04 17:50:33 -04:00
})
}
</Sidebar>
);
}
2019-05-27 13:26:29 -04:00
BuildingEdit.propTypes = {
user: PropTypes.object,
match: PropTypes.object,
building_id: PropTypes.string,
}
class EditForm extends Component {
2018-09-11 15:59:59 -04:00
constructor(props) {
super(props);
2018-10-20 10:35:52 -04:00
this.state = {}
2019-05-27 13:35:12 -04:00
for (const field of props.fields) {
2018-10-20 10:35:52 -04:00
this.state[field.slug] = props[field.slug]
}
this.state.error = this.props.error || undefined;
this.state.like = this.props.like || undefined;
2018-09-11 15:59:59 -04:00
this.handleChange = this.handleChange.bind(this);
2019-01-19 11:54:20 -05:00
this.handleCheck = this.handleCheck.bind(this);
2018-09-11 15:59:59 -04:00
this.handleLike = this.handleLike.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleUpdate = this.handleUpdate.bind(this);
2018-09-11 15:59:59 -04:00
}
/**
* Handle changes on typical inputs
* - e.g. input[type=text], radio, select, textare
*
* @param {DocumentEvent} event
*/
2018-09-11 15:59:59 -04:00
handleChange(event) {
const target = event.target;
2018-10-25 06:57:58 -04:00
let value = (target.value === '')? null : target.value;
2018-09-11 15:59:59 -04:00
const name = target.name;
2018-10-25 06:57:58 -04:00
// special transform - consider something data driven before adding 'else if's
if (name === 'location_postcode' && value !== null) {
value = value.toUpperCase();
}
2019-01-19 11:54:20 -05:00
this.setState({
[name]: value
});
}
/**
* Handle changes on checkboxes
* - e.g. input[type=checkbox]
*
* @param {DocumentEvent} event
*/
handleCheck(event) {
const target = event.target;
const value = target.checked;
const name = target.name;
2018-10-25 06:57:58 -04:00
2018-09-11 15:59:59 -04:00
this.setState({
[name]: value
});
}
/**
* Handle update directly
* - e.g. as callback from MultiTextInput where we set a list of strings
*
* @param {String} key
* @param {*} value
*/
handleUpdate(key, value) {
this.setState({
[key]: value
});
}
/**
* Handle likes separately
* - like/love reaction is limited to set/unset per user
*
* @param {DocumentEvent} event
*/
2018-09-11 15:59:59 -04:00
handleLike(event) {
event.preventDefault();
2019-01-22 12:52:32 -05:00
const like = event.target.checked;
2019-01-22 12:34:46 -05:00
fetch(`/building/${this.props.building_id}/like.json`, {
method: 'POST',
headers:{
2019-05-27 11:39:16 -04:00
'Content-Type': 'application/json'
},
2019-01-22 12:52:32 -05:00
credentials: 'same-origin',
body: JSON.stringify({like: like})
}).then(
res => res.json()
).then(function(res){
if (res.error) {
this.setState({error: res.error})
} else {
this.props.selectBuilding(res);
this.setState({
likes_total: res.likes_total
})
}
}.bind(this)).catch(
(err) => this.setState({error: err})
);
2018-09-11 15:59:59 -04:00
}
handleSubmit(event) {
event.preventDefault();
2018-09-13 12:13:03 -04:00
this.setState({error: undefined})
2018-09-30 16:54:47 -04:00
fetch(`/building/${this.props.building_id}.json`, {
2018-09-11 15:59:59 -04:00
method: 'POST',
body: JSON.stringify(this.state),
headers:{
2019-05-27 11:39:16 -04:00
'Content-Type': 'application/json'
},
credentials: 'same-origin'
2018-09-11 15:59:59 -04:00
}).then(
res => res.json()
).then(function(res){
if (res.error) {
2018-09-13 12:13:03 -04:00
this.setState({error: res.error})
2018-09-11 15:59:59 -04:00
} else {
this.props.selectBuilding(res);
2018-09-11 15:59:59 -04:00
}
2018-09-13 12:13:03 -04:00
}.bind(this)).catch(
(err) => this.setState({error: err})
2018-09-11 15:59:59 -04:00
);
}
render() {
2018-11-29 17:00:53 -05:00
const match = this.props.cat === this.props.slug;
2019-05-27 13:26:29 -04:00
const buildingLike = this.props.building_like;
2018-09-11 15:59:59 -04:00
return (
2019-05-27 11:31:48 -04:00
<section className={(this.props.inactive)? 'data-section inactive': 'data-section'}>
<header className={`section-header edit ${this.props.slug} ${(match? 'active' : '')}`}>
2018-11-29 17:00:53 -05:00
<NavLink
to={`/edit/${this.props.slug}/building/${this.props.building_id}.html`}
title={(this.props.inactive)? 'Coming soon… Click the ? for more info.' :
(match)? 'Hide details' : 'Show details'}
isActive={() => match}>
<h3 className="h3">{this.props.title}</h3>
</NavLink>
2018-10-05 04:10:20 -04:00
<nav className="icon-buttons">
2019-05-27 11:39:16 -04:00
{
this.props.help?
<a className="icon-button help" title="Find out more" href={this.props.help}>
2019-01-22 14:39:16 -05:00
Info
2019-05-27 11:39:16 -04:00
</a>
: null
}
{
(match && !this.props.inactive && this.props.slug !== 'like')? // special-case for likes
<NavLink className="icon-button save" title="Save Changes"
onClick={this.handleSubmit}
to={`/edit/${this.props.slug}/building/${this.props.building_id}.html`}>
Save
2019-05-27 11:39:16 -04:00
<SaveIcon />
</NavLink>
: null
}
2018-10-05 04:10:20 -04:00
</nav>
2018-10-04 17:50:33 -04:00
</header>
2018-11-29 17:00:53 -05:00
{
2019-05-27 11:39:16 -04:00
match? (
!this.props.inactive?
<form action={`/edit/${this.props.slug}/building/${this.props.building_id}.html`}
method="GET" onSubmit={this.handleSubmit}>
{
this.props.slug === 'location'?
<InfoBox msg="Text-based address fields are disabled at the moment. We're looking into how best to collect this data." />
: null
}
<ErrorBox msg={this.state.error} />
{
this.props.fields.map((props) => {
switch (props.type) {
case 'text':
return <TextInput {...props} handleChange={this.handleChange}
2019-01-22 14:39:16 -05:00
value={this.state[props.slug]} key={props.slug} />
2019-05-27 11:39:16 -04:00
case 'text_list':
return <TextListInput {...props} handleChange={this.handleChange}
2019-01-22 14:39:16 -05:00
value={this.state[props.slug]} key={props.slug} />
2019-05-27 11:39:16 -04:00
case 'text_long':
return <LongTextInput {...props} handleChange={this.handleChange}
2019-01-22 14:39:16 -05:00
value={this.state[props.slug]} key={props.slug} />
2019-05-27 11:39:16 -04:00
case 'number':
return <NumberInput {...props} handleChange={this.handleChange}
2019-01-22 14:39:16 -05:00
value={this.state[props.slug]} key={props.slug} />
2019-05-27 11:39:16 -04:00
case 'year_estimator':
return <YearEstimator {...props} handleChange={this.handleChange}
2019-04-04 08:05:58 -04:00
value={this.state[props.slug]} key={props.slug} />
2019-05-27 11:39:16 -04:00
case 'text_multi':
return <MultiTextInput {...props} handleChange={this.handleUpdate}
2019-01-22 14:39:16 -05:00
value={this.state[props.slug]} key={props.slug} />
2019-05-27 11:39:16 -04:00
case 'checkbox':
return <CheckboxInput {...props} handleChange={this.handleCheck}
2019-01-22 14:39:16 -05:00
value={this.state[props.slug]} key={props.slug} />
2019-05-27 11:39:16 -04:00
case 'like':
return <LikeButton {...props} handleLike={this.handleLike}
2019-05-27 13:26:29 -04:00
building_like={buildingLike}
2019-01-22 14:39:16 -05:00
value={this.state[props.slug]} key={props.slug} />
2019-05-27 11:39:16 -04:00
default:
return null
}
})
2019-01-22 14:39:16 -05:00
}
2019-05-27 11:39:16 -04:00
<InfoBox msg="Colouring may take a few seconds - try zooming the map or hitting refresh after saving (we're working on making this smoother)." />
{
(this.props.slug === 'like')? // special-case for likes
null :
<div className="buttons-container">
<button type="submit" className="btn btn-primary">Save</button>
</div>
}
</form>
: <form><InfoBox msg={`We're not collection data on ${this.props.title.toLowerCase()} yet - check back soon.`} /></form>
) : null
}
</section>
)
2018-09-11 15:59:59 -04:00
}
}
2019-05-27 13:26:29 -04:00
EditForm.propTypes = {
title: PropTypes.string,
slug: PropTypes.string,
cat: PropTypes.string,
help: PropTypes.string,
error: PropTypes.object,
like: PropTypes.bool,
building_like: PropTypes.bool,
selectBuilding: PropTypes.func,
building_id: PropTypes.string,
inactive: PropTypes.bool,
fields: PropTypes.array
}
const TextInput = (props) => (
<Fragment>
2018-10-05 13:41:12 -04:00
<Label slug={props.slug} title={props.title} tooltip={props.tooltip} />
<input className="form-control" type="text"
id={props.slug} name={props.slug}
2019-05-27 11:31:48 -04:00
value={props.value || ''}
2019-01-19 11:54:20 -05:00
maxLength={props.max_length}
2018-10-05 17:19:26 -04:00
disabled={props.disabled}
placeholder={props.placeholder}
onChange={props.handleChange}
2019-05-27 11:39:16 -04:00
/>
</Fragment>
);
2019-05-27 13:26:29 -04:00
TextInput.propTypes = {
slug: PropTypes.string,
title: PropTypes.string,
tooltip: PropTypes.string,
value: PropTypes.string,
max_length: PropTypes.number,
disabled: PropTypes.bool,
placeholder: PropTypes.string,
handleChange: PropTypes.func
}
const LongTextInput = (props) => (
<Fragment>
<Label slug={props.slug} title={props.title} tooltip={props.tooltip} />
<textarea className="form-control"
id={props.slug} name={props.slug}
disabled={props.disabled}
placeholder={props.placeholder}
onChange={props.handleChange}
2019-05-27 11:31:48 -04:00
value={props.value || ''}></textarea>
</Fragment>
)
2019-05-27 13:26:29 -04:00
LongTextInput.propTypes = {
slug: PropTypes.string,
title: PropTypes.string,
tooltip: PropTypes.string,
value: PropTypes.string,
disabled: PropTypes.bool,
placeholder: PropTypes.string,
handleChange: PropTypes.func
}
class MultiTextInput extends Component {
constructor(props) {
super(props);
this.edit = this.edit.bind(this);
this.add = this.add.bind(this);
this.remove = this.remove.bind(this);
this.getValues = this.getValues.bind(this);
}
getValues() {
return (this.props.value && this.props.value.length)? this.props.value : [null];
}
edit(event) {
2019-05-27 13:26:29 -04:00
const editIndex = +event.target.dataset.index;
const editItem = event.target.value;
const oldValues = this.getValues();
const values = oldValues.map((item, i) => {
return i === editIndex ? editItem : item;
});
this.props.handleChange(this.props.slug, values);
}
add(event) {
event.preventDefault();
2019-05-27 11:31:48 -04:00
const values = this.getValues().concat('');
this.props.handleChange(this.props.slug, values);
}
remove(event){
2019-05-27 13:26:29 -04:00
const removeIndex = +event.target.dataset.index;
const values = this.getValues().filter((_, i) => {
2019-05-27 13:26:29 -04:00
return i !== removeIndex;
});
this.props.handleChange(this.props.slug, values);
}
render() {
const values = this.getValues();
return (
2019-05-27 11:39:16 -04:00
<Fragment>
<Label slug={this.props.slug} title={this.props.title} tooltip={this.props.tooltip} />
{
values.map((item, i) => (
<div className="input-group" key={i}>
<input className="form-control" type="text"
key={`${this.props.slug}-${i}`} name={`${this.props.slug}-${i}`}
data-index={i}
value={item || ''}
placeholder={this.props.placeholder}
disabled={this.props.disabled}
onChange={this.edit}
/>
<div className="input-group-append">
<button type="button" onClick={this.remove}
title="Remove"
data-index={i} className="btn btn-outline-dark"></button>
</div>
</div>
))
}
<button type="button" title="Add" onClick={this.add}
2019-04-18 12:07:13 -04:00
className="btn btn-outline-dark">+</button>
2019-05-27 11:39:16 -04:00
</Fragment>
)
}
}
2019-05-27 13:26:29 -04:00
MultiTextInput.propTypes = {
slug: PropTypes.string,
title: PropTypes.string,
tooltip: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.string),
placeholder: PropTypes.string,
disabled: PropTypes.bool,
handleChange: PropTypes.func
}
2018-10-05 13:41:12 -04:00
const TextListInput = (props) => (
<Fragment>
<Label slug={props.slug} title={props.title} tooltip={props.tooltip} />
<select className="form-control"
id={props.slug} name={props.slug}
2019-05-27 11:31:48 -04:00
value={props.value || ''}
2018-10-05 17:19:26 -04:00
disabled={props.disabled}
2018-10-05 13:41:12 -04:00
list={`${props.slug}_suggestions`}
onChange={props.handleChange}>
<option value="">Select a source</option>
{
props.options.map(option => (
2018-10-05 17:19:26 -04:00
<option key={option} value={option}>{option}</option>
2018-10-05 13:41:12 -04:00
))
}
</select>
</Fragment>
)
2019-05-27 13:26:29 -04:00
TextListInput.propTypes = {
slug: PropTypes.string,
title: PropTypes.string,
tooltip: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.string),
value: PropTypes.string,
disabled: PropTypes.bool,
handleChange: PropTypes.func
}
const NumberInput = (props) => (
<Fragment>
2018-10-05 13:41:12 -04:00
<Label slug={props.slug} title={props.title} tooltip={props.tooltip} />
<input className="form-control" type="number" step={props.step}
id={props.slug} name={props.slug}
2019-05-27 11:31:48 -04:00
value={props.value || ''}
2018-10-05 17:19:26 -04:00
disabled={props.disabled}
onChange={props.handleChange}
2019-05-27 11:39:16 -04:00
/>
</Fragment>
);
2019-05-27 13:26:29 -04:00
NumberInput.propTypes = {
slug: PropTypes.string,
title: PropTypes.string,
tooltip: PropTypes.string,
step: PropTypes.number,
value: PropTypes.number,
disabled: PropTypes.bool,
handleChange: PropTypes.func
}
2019-04-04 08:05:58 -04:00
class YearEstimator extends Component {
constructor(props) {
super(props);
2019-04-18 12:07:13 -04:00
this.state = {
year: props.date_year,
upper: props.date_upper,
lower: props.date_lower,
decade: Math.floor(props.date_year / 10) * 10,
century: Math.floor(props.date_year / 100) * 100
}
2019-04-04 08:05:58 -04:00
}
// TODO add dropdown for decade, century
// TODO roll in first/last year estimate
// TODO handle changes internally, reporting out date_year, date_upper, date_lower
render() {
return (
<NumberInput {...this.props} handleChange={this.props.handleChange}
2019-05-27 11:39:16 -04:00
value={this.props.value} key={this.props.slug} />
2019-04-04 08:05:58 -04:00
)
}
}
2019-05-27 13:26:29 -04:00
YearEstimator.propTypes = {
slug: PropTypes.string,
title: PropTypes.string,
tooltip: PropTypes.string,
date_year: PropTypes.number,
date_upper: PropTypes.number,
date_lower: PropTypes.number,
value: PropTypes.number,
disabled: PropTypes.bool,
handleChange: PropTypes.func
}
2019-01-19 11:54:20 -05:00
const CheckboxInput = (props) => (
2019-01-22 12:52:32 -05:00
<div className="form-check">
2019-01-19 11:54:20 -05:00
<input className="form-check-input" type="checkbox"
id={props.slug} name={props.slug}
checked={!!props.value}
disabled={props.disabled}
onChange={props.handleChange}
2019-05-27 11:39:16 -04:00
/>
2019-01-19 11:54:20 -05:00
<label htmlFor={props.slug} className="form-check-label">
{props.title}
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
</label>
</div>
)
2019-05-27 13:26:29 -04:00
CheckboxInput.propTypes = {
slug: PropTypes.string,
title: PropTypes.string,
tooltip: PropTypes.string,
value: PropTypes.bool,
disabled: PropTypes.bool,
handleChange: PropTypes.func
}
const LikeButton = (props) => (
<Fragment>
2018-10-25 08:49:02 -04:00
<p className="likes">{(props.value)? props.value : 0} likes</p>
2019-01-22 12:52:32 -05:00
<div className="form-check">
<input className="form-check-input" type="checkbox"
id={props.slug} name={props.slug}
checked={!!props.building_like}
disabled={props.disabled}
onChange={props.handleLike}
2019-05-27 11:39:16 -04:00
/>
2019-01-22 12:52:32 -05:00
<label htmlFor={props.slug} className="form-check-label">
2019-04-04 08:57:45 -04:00
I like this building and think it contributes to the city!
2019-01-22 12:52:32 -05:00
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
</label>
</div>
</Fragment>
);
2019-05-27 13:26:29 -04:00
LikeButton.propTypes = {
slug: PropTypes.string,
title: PropTypes.string,
tooltip: PropTypes.string,
value: PropTypes.number,
building_like: PropTypes.bool,
disabled: PropTypes.bool,
handleLike: PropTypes.func
}
2018-10-05 13:41:12 -04:00
const Label = (props) => (
<label htmlFor={props.slug}>
{props.title}
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
</label>
2019-05-27 13:26:29 -04:00
);
Label.propTypes = {
slug: PropTypes.string,
title: PropTypes.string,
tooltip: PropTypes.string
}
2018-10-05 13:41:12 -04:00
export default BuildingEdit;