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

369 lines
14 KiB
TypeScript
Raw Normal View History

import React, { Fragment } from 'react';
import { Link, NavLink } from 'react-router-dom';
2019-05-27 13:26:29 -04:00
import PropTypes from 'prop-types';
2018-09-11 15:59:59 -04:00
import Sidebar from './sidebar';
import Tooltip from './components/tooltip';
import InfoBox from './components/info-box';
import { EditIcon } from './components/icons';
2019-08-06 17:11:17 -04:00
import { sanitiseURL } from './helpers';
2018-09-11 15:59:59 -04:00
import CONFIG from './fields-config.json';
const BuildingView = (props) => {
if (!props.building_id){
return (
<Sidebar title="Building Not Found">
<InfoBox msg="We can't find that one anywhere - try the map again?" />
<div className="buttons-container with-space">
2018-11-29 17:00:53 -05:00
<Link to="/view/age.html" className="btn btn-secondary">Back to maps</Link>
</div>
</Sidebar>
);
}
2019-05-10 09:00:20 -04:00
const cat = props.match.params.cat;
return (
2019-05-27 11:31:48 -04:00
<Sidebar title={'Data available for this building'} back={`/view/${cat}.html`}>
{
2019-05-27 13:26:29 -04:00
CONFIG.map(section => (
<DataSection
2019-05-27 13:26:29 -04:00
key={section.slug} cat={cat}
building_id={props.building_id}
2019-08-01 06:19:04 -04:00
{...section} {...props} />
))
}
</Sidebar>
);
}
2019-05-27 13:26:29 -04:00
BuildingView.propTypes = {
2019-05-27 15:28:28 -04:00
building_id: PropTypes.number,
2019-05-27 13:26:29 -04:00
match: PropTypes.object,
uprns: PropTypes.arrayOf(PropTypes.shape({
uprn: PropTypes.string.isRequired,
parent_uprn: PropTypes.string
})),
building_like: PropTypes.bool
}
class DataSection extends React.Component<any, any> { // TODO: add proper types
static propTypes = { // TODO: generate propTypes from TS
title: PropTypes.string,
cat: PropTypes.string,
slug: PropTypes.string,
intro: PropTypes.string,
help: PropTypes.string,
inactive: PropTypes.bool,
building_id: PropTypes.number,
children: PropTypes.node
};
2019-08-01 06:19:04 -04:00
constructor(props) {
super(props);
this.state = {
copying: false,
values_to_copy: {}
};
this.toggleCopying = this.toggleCopying.bind(this);
this.toggleCopyAttribute = this.toggleCopyAttribute.bind(this);
}
/**
* Enter or exit "copying" state - allow user to select attributes to copy
*/
toggleCopying() {
this.setState({
copying: !this.state.copying
})
}
/**
* Keep track of data to copy (accumulate while in "copying" state)
*
* @param {string} key
*/
toggleCopyAttribute(key) {
const value = this.props[key];
const values = this.state.values_to_copy;
if(Object.keys(this.state.values_to_copy).includes(key)){
delete values[key];
} else {
values[key] = value;
}
this.setState({
values_to_copy: values
})
}
render() {
const props = this.props;
const match = props.cat === props.slug;
const data_string = JSON.stringify(this.state.values_to_copy);
return (
<section id={props.slug} className={(props.inactive)? 'data-section inactive': 'data-section'}>
<header className={`section-header view ${props.slug} ${(match? 'active' : '')}`}>
<NavLink
to={`/view/${props.slug}/building/${props.building_id}.html`}
title={(props.inactive)? 'Coming soon… Click the ? for more info.' :
(match)? 'Hide details' : 'Show details'}
isActive={() => match}>
<h3 className="h3">{props.title}</h3>
</NavLink>
<nav className="icon-buttons">
{
(match && !props.inactive)?
this.state.copying?
<Fragment>
<NavLink
to={`/multi-edit/${props.cat}.html?data=${data_string}`}
className="icon-button copy">
Copy selected
</NavLink>
<a className="icon-button copy" onClick={this.toggleCopying}>Cancel</a>
</Fragment>
:
<a className="icon-button copy" onClick={this.toggleCopying}>Copy</a>
2019-05-27 11:39:16 -04:00
: null
2019-08-01 06:19:04 -04:00
}
{
props.help && !this.state.copying?
<a className="icon-button help" title="Find out more" href={props.help}>
Info
</a>
: null
}
{
!props.inactive && !this.state.copying?
<NavLink className="icon-button edit" title="Edit data"
to={`/edit/${props.slug}/building/${props.building_id}.html`}>
Edit
<EditIcon />
</NavLink>
: null
}
</nav>
</header>
{
match?
2019-05-27 11:39:16 -04:00
!props.inactive?
2019-08-01 06:19:04 -04:00
<dl className="data-list">
{
props.fields.map(field => {
switch (field.type) {
case 'uprn_list':
return <UPRNsDataEntry
key={field.slug}
title={field.title}
value={props.uprns}
tooltip={field.tooltip} />
case 'text_multi':
return <MultiDataEntry
key={field.slug}
2019-08-06 17:11:17 -04:00
slug={field.slug}
disabled={field.disabled}
cat={props.cat}
2019-08-01 06:19:04 -04:00
title={field.title}
value={props[field.slug]}
2019-08-06 17:11:17 -04:00
copying={this.state.copying}
toggleCopyAttribute={this.toggleCopyAttribute}
copy={Object.keys(this.state.values_to_copy).includes(field.slug)}
2019-08-01 06:19:04 -04:00
tooltip={field.tooltip} />
case 'like':
return <LikeDataEntry
key={field.slug}
title={field.title}
value={props[field.slug]}
user_building_like={props.building_like}
tooltip={field.tooltip} />
default:
return <DataEntry
key={field.slug}
slug={field.slug}
disabled={field.disabled}
cat={props.cat}
title={field.title}
value={props[field.slug]}
copying={this.state.copying}
toggleCopyAttribute={this.toggleCopyAttribute}
copy={Object.keys(this.state.values_to_copy).includes(field.slug)}
tooltip={field.tooltip} />
}
})
}
</dl>
: <p className="data-intro">{props.intro}</p>
: null
}
</section>
);
}
}
const DataEntry: React.FunctionComponent<any> = (props) => { // TODO: remove any
return (
<Fragment>
<dt>
{ props.title }
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
2019-08-01 06:19:04 -04:00
{ (props.copying && props.cat && props.slug && !props.disabled)?
<div className="icon-buttons">
2019-08-01 06:19:04 -04:00
<label className="icon-button copy">
Copy
2019-08-01 06:19:04 -04:00
<input type="checkbox" checked={props.copy}
onChange={() => props.toggleCopyAttribute(props.slug)}/>
</label>
</div>
: null
}
</dt>
<dd>{
(props.value != null && props.value !== '')?
(typeof(props.value) === 'boolean')?
(props.value)? 'Yes' : 'No'
: props.value
: '\u00A0'}</dd>
</Fragment>
);
}
2019-05-27 13:26:29 -04:00
DataEntry.propTypes = {
title: PropTypes.string,
2019-05-27 15:13:43 -04:00
cat: PropTypes.string,
slug: PropTypes.string,
2019-05-27 13:26:29 -04:00
tooltip: PropTypes.string,
disabled: PropTypes.bool,
2019-05-27 13:26:29 -04:00
value: PropTypes.any
}
const LikeDataEntry: React.FunctionComponent<any> = (props) => { // TODO: remove any
const data_string = JSON.stringify({like: true});
return (
<Fragment>
<dt>
{ props.title }
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
<div className="icon-buttons">
<NavLink
to={`/multi-edit/${props.cat}.html?data=${data_string}`}
className="icon-button copy">
Copy
</NavLink>
</div>
</dt>
<dd>
{
(props.value != null)?
(props.value === 1)?
`${props.value} person likes this building`
: `${props.value} people like this building`
: '\u00A0'
}
</dd>
2019-05-27 11:39:16 -04:00
{
(props.user_building_like)? <dd>&hellip;including you!</dd> : ''
2019-05-27 11:39:16 -04:00
}
</Fragment>
);
}
2019-05-27 13:26:29 -04:00
LikeDataEntry.propTypes = {
title: PropTypes.string,
2019-05-27 15:13:43 -04:00
cat: PropTypes.string,
2019-05-27 13:26:29 -04:00
tooltip: PropTypes.string,
value: PropTypes.any,
user_building_like: PropTypes.bool
}
const MultiDataEntry: React.FunctionComponent<any> = (props) => { // TODO: remove any
2019-01-19 13:47:08 -05:00
let content;
if (props.value && props.value.length) {
content = <ul>{
props.value.map((item, index) => {
2019-05-27 13:26:29 -04:00
return <li key={index}><a href={sanitiseURL(item)}>{item}</a></li>
2019-01-19 13:47:08 -05:00
})
}</ul>
} else {
content = '\u00A0'
}
return (
<Fragment>
<dt>
{ props.title }
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
2019-08-06 17:11:17 -04:00
{ (props.copying && props.cat && props.slug && !props.disabled)?
<div className="icon-buttons">
<label className="icon-button copy">
Copy
<input type="checkbox" checked={props.copy}
onChange={() => props.toggleCopyAttribute(props.slug)}/>
</label>
</div>
: null
}
2019-01-19 13:47:08 -05:00
</dt>
<dd>{ content }</dd>
</Fragment>
);
}
2019-05-27 13:26:29 -04:00
MultiDataEntry.propTypes = {
title: PropTypes.string,
tooltip: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.string)
}
const UPRNsDataEntry = (props) => {
const uprns = props.value || [];
2019-05-27 13:26:29 -04:00
const noParent = uprns.filter(uprn => uprn.parent_uprn == null);
const withParent = uprns.filter(uprn => uprn.parent_uprn != null);
return (
2019-05-27 11:39:16 -04:00
<Fragment>
<dt>
{ props.title }
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
</dt>
<dd><ul className="uprn-list">
<Fragment>{
2019-05-27 13:26:29 -04:00
noParent.length?
noParent.map(uprn => (
2019-05-27 11:39:16 -04:00
<li key={uprn.uprn}>{uprn.uprn}</li>
))
2019-05-27 11:39:16 -04:00
: '\u00A0'
}</Fragment>
{
2019-05-27 13:26:29 -04:00
withParent.length?
2019-05-27 11:39:16 -04:00
<details>
<summary>Children</summary>
{
2019-05-27 13:26:29 -04:00
withParent.map(uprn => (
2019-05-27 11:39:16 -04:00
<li key={uprn.uprn}>{uprn.uprn} (child of {uprn.parent_uprn})</li>
))
}
</details>
: null
}
</ul></dd>
</Fragment>
)
}
2019-05-27 13:26:29 -04:00
UPRNsDataEntry.propTypes = {
title: PropTypes.string,
tooltip: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.shape({
uprn: PropTypes.string.isRequired,
parent_uprn: PropTypes.string
}))
}
export default BuildingView;