import React, { Component, Fragment } from 'react'; import { Link, NavLink, Redirect } from 'react-router-dom'; import ErrorBox from './error-box'; import InfoBox from './info-box'; import Sidebar from './sidebar'; import Tooltip from './tooltip'; import { SaveIcon } from './icons'; import { parseCategoryURL } from '../parse'; import CONFIG from './fields-config.json'; const BuildingEdit = (props) => { if (!props.user){ return <Redirect to="/sign-up.html" /> } const cat = parseCategoryURL(props.match.url); if (!props.building_id){ return ( <Sidebar title="Building Not Found" back={`/edit/${cat}.html`}> <InfoBox msg="We can't find that one anywhere - try the map again?" /> <div className="buttons-container"> <Link to={`/edit/${cat}.html`} className="btn btn-secondary">Back to maps</Link> </div> </Sidebar> ); } return ( <Sidebar key={props.building_id} title={`You are editing`} back={`/edit/${cat}.html`}> { CONFIG.map((conf_props) => { return <EditForm {...conf_props} {...props} cat={cat} key={conf_props.slug} /> }) } </Sidebar> ); } class EditForm extends Component { constructor(props) { super(props); this.state = {} for (let field of props.fields) { this.state[field.slug] = props[field.slug] } this.state.error = this.props.error || undefined; this.state.like = this.props.like || undefined; this.handleChange = this.handleChange.bind(this); this.handleCheck = this.handleCheck.bind(this); this.handleLike = this.handleLike.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.handleUpdate = this.handleUpdate.bind(this); } /** * Handle changes on typical inputs * - e.g. input[type=text], radio, select, textare * * @param {DocumentEvent} event */ handleChange(event) { const target = event.target; let value = (target.value === '')? null : target.value; const name = target.name; // special transform - consider something data driven before adding 'else if's if (name === 'location_postcode' && value !== null) { value = value.toUpperCase(); } 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; 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 */ handleLike(event) { event.preventDefault(); const like = event.target.checked; fetch(`/building/${this.props.building_id}/like.json`, { method: 'POST', headers:{ 'Content-Type': 'application/json' }, 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}) ); } handleSubmit(event) { event.preventDefault(); this.setState({error: undefined}) fetch(`/building/${this.props.building_id}.json`, { method: 'POST', body: JSON.stringify(this.state), headers:{ 'Content-Type': 'application/json' }, credentials: 'same-origin' }).then( res => res.json() ).then(function(res){ if (res.error) { this.setState({error: res.error}) } else { this.props.selectBuilding(res); } }.bind(this)).catch( (err) => this.setState({error: err}) ); } render() { const match = this.props.cat === this.props.slug; const building_like = this.props.building_like; return ( <section className={(this.props.inactive)? "data-section inactive": "data-section"}> <header className={`section-header edit ${this.props.slug} ${(match? "active" : "")}`}> <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> <nav className="icon-buttons"> { this.props.help? <a className="icon-button help" title="Find out more" href={this.props.help}> Info </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 <SaveIcon /> </NavLink> : null } </nav> </header> { 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} value={this.state[props.slug]} key={props.slug} /> case "text_list": return <TextListInput {...props} handleChange={this.handleChange} value={this.state[props.slug]} key={props.slug} /> case "text_long": return <LongTextInput {...props} handleChange={this.handleChange} value={this.state[props.slug]} key={props.slug} /> case "number": return <NumberInput {...props} handleChange={this.handleChange} value={this.state[props.slug]} key={props.slug} /> case "year_estimator": return <YearEstimator {...props} handleChange={this.handleChange} value={this.state[props.slug]} key={props.slug} /> case "text_multi": return <MultiTextInput {...props} handleChange={this.handleUpdate} value={this.state[props.slug]} key={props.slug} /> case "checkbox": return <CheckboxInput {...props} handleChange={this.handleCheck} value={this.state[props.slug]} key={props.slug} /> case "like": return <LikeButton {...props} handleLike={this.handleLike} building_like={building_like} value={this.state[props.slug]} key={props.slug} /> default: return null } }) } <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> ) } } const TextInput = (props) => ( <Fragment> <Label slug={props.slug} title={props.title} tooltip={props.tooltip} /> <input className="form-control" type="text" id={props.slug} name={props.slug} value={props.value || ""} maxLength={props.max_length} disabled={props.disabled} placeholder={props.placeholder} onChange={props.handleChange} /> </Fragment> ); 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} value={props.value || ""}></textarea> </Fragment> ) 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) { const edit_i = +event.target.dataset.index; const edit_item = event.target.value; const old_values = this.getValues(); const values = old_values.map((item, i) => { return i === edit_i ? edit_item : item; }); this.props.handleChange(this.props.slug, values); } add(event) { event.preventDefault(); const values = this.getValues().concat(""); this.props.handleChange(this.props.slug, values); } remove(event){ const remove_i = +event.target.dataset.index; const values = this.getValues().filter((_, i) => { return i !== remove_i; }); this.props.handleChange(this.props.slug, values); } render() { const values = this.getValues(); return ( <Fragment> <Label slug={this.props.slug} title={this.props.title} tooltip={this.props.tooltip} /> { values.map((item, i) => ( <div class="input-group"> <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 class="input-group-append"> <button type="button" onClick={this.remove} title="Remove" data-index={i} class="btn btn-outline-dark">✕</button> </div> </div> )) } <button type="button" title="Add" onClick={this.add} class="btn btn-outline-dark">+</button> </Fragment> ) } } const TextListInput = (props) => ( <Fragment> <Label slug={props.slug} title={props.title} tooltip={props.tooltip} /> <select className="form-control" id={props.slug} name={props.slug} value={props.value || ""} disabled={props.disabled} list={`${props.slug}_suggestions`} onChange={props.handleChange}> <option value="">Select a source</option> { props.options.map(option => ( <option key={option} value={option}>{option}</option> )) } </select> </Fragment> ) const NumberInput = (props) => ( <Fragment> <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} value={props.value || ""} disabled={props.disabled} onChange={props.handleChange} /> </Fragment> ); class YearEstimator extends Component { constructor(props) { super(props); } // 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} value={this.props.value} key={this.props.slug} /> ) } } const CheckboxInput = (props) => ( <div className="form-check"> <input className="form-check-input" type="checkbox" id={props.slug} name={props.slug} checked={!!props.value} disabled={props.disabled} onChange={props.handleChange} /> <label htmlFor={props.slug} className="form-check-label"> {props.title} { props.tooltip? <Tooltip text={ props.tooltip } /> : null } </label> </div> ) const LikeButton = (props) => ( <Fragment> <p className="likes">{(props.value)? props.value : 0} likes</p> <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} /> <label htmlFor={props.slug} className="form-check-label"> I like this building and think it contributes to the city! { props.tooltip? <Tooltip text={ props.tooltip } /> : null } </label> </div> </Fragment> ); const Label = (props) => ( <label htmlFor={props.slug}> {props.title} { props.tooltip? <Tooltip text={ props.tooltip } /> : null } </label> ) export default BuildingEdit;