2019-08-21 17:20:31 -04:00
|
|
|
import React, { Fragment } from 'react';
|
2019-11-07 02:39:26 -05:00
|
|
|
import { NavLink, Redirect } from 'react-router-dom';
|
2020-08-04 10:54:49 -04:00
|
|
|
import Confetti from 'canvas-confetti';
|
2019-08-14 16:54:00 -04:00
|
|
|
|
2020-01-02 05:59:13 -05:00
|
|
|
import { apiPost } from '../apiHelpers';
|
2019-08-21 17:20:31 -04:00
|
|
|
import ErrorBox from '../components/error-box';
|
|
|
|
import InfoBox from '../components/info-box';
|
2019-11-07 02:39:26 -05:00
|
|
|
import { compareObjects } from '../helpers';
|
2021-02-22 01:59:24 -05:00
|
|
|
import { Building, UserVerified } from '../models/building';
|
2019-10-15 09:37:23 -04:00
|
|
|
import { User } from '../models/user';
|
2019-11-07 02:39:26 -05:00
|
|
|
|
|
|
|
import ContainerHeader from './container-header';
|
2019-10-18 10:06:50 -04:00
|
|
|
import { CategoryViewProps, CopyProps } from './data-containers/category-view-props';
|
2019-11-07 02:39:26 -05:00
|
|
|
import { CopyControl } from './header-buttons/copy-control';
|
|
|
|
import { ViewEditControl } from './header-buttons/view-edit-control';
|
2019-10-15 09:37:23 -04:00
|
|
|
|
|
|
|
interface DataContainerProps {
|
|
|
|
title: string;
|
|
|
|
cat: string;
|
|
|
|
intro: string;
|
|
|
|
help: string;
|
|
|
|
inactive?: boolean;
|
|
|
|
|
2019-11-05 15:13:10 -05:00
|
|
|
user?: User;
|
2019-10-30 08:28:10 -04:00
|
|
|
mode: 'view' | 'edit';
|
2019-11-05 15:13:10 -05:00
|
|
|
building?: Building;
|
|
|
|
building_like?: boolean;
|
2020-08-04 10:54:49 -04:00
|
|
|
user_verified?: any;
|
2021-02-22 01:59:24 -05:00
|
|
|
onBuildingUpdate: (buildingId: number, updatedData: Building) => void;
|
|
|
|
onBuildingLikeUpdate: (buildingId: number, updatedData: boolean) => void;
|
|
|
|
onUserVerifiedUpdate: (buildingId: number, updatedData: UserVerified) => void;
|
2019-10-15 09:37:23 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
interface DataContainerState {
|
|
|
|
error: string;
|
|
|
|
copying: boolean;
|
2019-10-15 10:44:22 -04:00
|
|
|
keys_to_copy: {[key: string]: boolean};
|
2019-10-15 14:16:48 -04:00
|
|
|
currentBuildingId: number;
|
2019-10-21 06:20:10 -04:00
|
|
|
currentBuildingRevisionId: number;
|
2019-10-15 14:16:48 -04:00
|
|
|
buildingEdits: Partial<Building>;
|
2019-10-15 09:37:23 -04:00
|
|
|
}
|
2019-08-14 16:54:00 -04:00
|
|
|
|
2021-02-22 01:59:24 -05:00
|
|
|
export type DataContainerType = React.ComponentType<DataContainerProps>;
|
|
|
|
|
2019-08-14 16:54:00 -04:00
|
|
|
/**
|
|
|
|
* Shared functionality for view/edit forms
|
|
|
|
*
|
|
|
|
* See React Higher-order-component docs for the pattern
|
|
|
|
* - https://reactjs.org/docs/higher-order-components.html
|
|
|
|
*
|
|
|
|
* @param WrappedComponent
|
|
|
|
*/
|
2021-02-22 01:59:24 -05:00
|
|
|
const withCopyEdit: (wc: React.ComponentType<CategoryViewProps>) => DataContainerType = (WrappedComponent: React.ComponentType<CategoryViewProps>) => {
|
2019-10-17 12:07:34 -04:00
|
|
|
return class DataContainer extends React.Component<DataContainerProps, DataContainerState> {
|
2019-08-14 16:54:00 -04:00
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
2019-08-21 17:20:31 -04:00
|
|
|
|
2019-08-14 16:54:00 -04:00
|
|
|
this.state = {
|
2019-10-15 09:37:23 -04:00
|
|
|
error: undefined,
|
2019-08-14 16:54:00 -04:00
|
|
|
copying: false,
|
2019-08-21 17:20:31 -04:00
|
|
|
keys_to_copy: {},
|
2019-10-15 14:16:48 -04:00
|
|
|
buildingEdits: {},
|
2019-10-21 06:20:10 -04:00
|
|
|
currentBuildingId: undefined,
|
|
|
|
currentBuildingRevisionId: undefined
|
2019-08-14 16:54:00 -04:00
|
|
|
};
|
2019-08-21 17:20:31 -04:00
|
|
|
|
|
|
|
this.handleChange = this.handleChange.bind(this);
|
2019-10-21 06:20:10 -04:00
|
|
|
this.handleReset = this.handleReset.bind(this);
|
2019-08-21 17:20:31 -04:00
|
|
|
this.handleLike = this.handleLike.bind(this);
|
|
|
|
this.handleSubmit = this.handleSubmit.bind(this);
|
2020-08-04 10:54:49 -04:00
|
|
|
this.handleVerify = this.handleVerify.bind(this);
|
2019-08-21 17:20:31 -04:00
|
|
|
|
2019-08-14 16:54:00 -04:00
|
|
|
this.toggleCopying = this.toggleCopying.bind(this);
|
|
|
|
this.toggleCopyAttribute = this.toggleCopyAttribute.bind(this);
|
|
|
|
}
|
|
|
|
|
2019-10-15 14:16:48 -04:00
|
|
|
static getDerivedStateFromProps(props, state) {
|
2019-10-16 08:11:25 -04:00
|
|
|
const newBuildingId = props.building == undefined ? undefined : props.building.building_id;
|
2019-10-21 06:20:10 -04:00
|
|
|
const newBuildingRevisionId = props.building == undefined ? undefined : props.building.revision_id;
|
|
|
|
if(newBuildingId !== state.currentBuildingId || newBuildingRevisionId > state.currentBuildingRevisionId) {
|
2019-10-15 14:16:48 -04:00
|
|
|
return {
|
2019-10-16 08:11:25 -04:00
|
|
|
error: undefined,
|
|
|
|
copying: false,
|
|
|
|
keys_to_copy: {},
|
2019-10-15 14:16:48 -04:00
|
|
|
buildingEdits: {},
|
2019-10-21 06:20:10 -04:00
|
|
|
currentBuildingId: newBuildingId,
|
|
|
|
currentBuildingRevisionId: newBuildingRevisionId
|
2019-10-15 14:16:48 -04:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-08-14 16:54:00 -04:00
|
|
|
/**
|
|
|
|
* Enter or exit "copying" state - allow user to select attributes to copy
|
|
|
|
*/
|
|
|
|
toggleCopying() {
|
|
|
|
this.setState({
|
|
|
|
copying: !this.state.copying
|
2019-11-07 03:13:30 -05:00
|
|
|
});
|
2019-08-14 16:54:00 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Keep track of data to copy (accumulate while in "copying" state)
|
|
|
|
*
|
|
|
|
* @param {string} key
|
|
|
|
*/
|
2019-08-21 17:20:31 -04:00
|
|
|
toggleCopyAttribute(key: string) {
|
2019-10-15 09:37:23 -04:00
|
|
|
const keys = {...this.state.keys_to_copy};
|
2019-08-21 17:20:31 -04:00
|
|
|
if(this.state.keys_to_copy[key]){
|
|
|
|
delete keys[key];
|
2019-08-14 16:54:00 -04:00
|
|
|
} else {
|
2019-08-21 17:20:31 -04:00
|
|
|
keys[key] = true;
|
2019-08-14 16:54:00 -04:00
|
|
|
}
|
|
|
|
this.setState({
|
2019-08-21 17:20:31 -04:00
|
|
|
keys_to_copy: keys
|
2019-11-07 03:13:30 -05:00
|
|
|
});
|
2019-08-14 16:54:00 -04:00
|
|
|
}
|
|
|
|
|
2019-10-15 14:16:48 -04:00
|
|
|
isEdited() {
|
|
|
|
const edits = this.state.buildingEdits;
|
|
|
|
// check if the edits object has any fields
|
|
|
|
return Object.entries(edits).length !== 0;
|
|
|
|
}
|
2019-08-23 12:35:17 -04:00
|
|
|
|
2019-10-21 06:20:10 -04:00
|
|
|
clearEdits() {
|
2019-08-23 12:35:17 -04:00
|
|
|
this.setState({
|
2019-10-21 06:20:10 -04:00
|
|
|
buildingEdits: {}
|
2019-08-23 12:35:17 -04:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-10-15 14:16:48 -04:00
|
|
|
getEditedBuilding() {
|
|
|
|
if(this.isEdited()) {
|
|
|
|
return Object.assign({}, this.props.building, this.state.buildingEdits);
|
|
|
|
} else {
|
|
|
|
return {...this.props.building};
|
2019-08-21 17:20:31 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-15 14:16:48 -04:00
|
|
|
updateBuildingState(key: string, value: any) {
|
|
|
|
const newBuilding = this.getEditedBuilding();
|
|
|
|
newBuilding[key] = value;
|
|
|
|
const [forwardPatch] = compareObjects(this.props.building, newBuilding);
|
2019-08-21 17:20:31 -04:00
|
|
|
|
2019-08-23 12:35:17 -04:00
|
|
|
this.setState({
|
2019-10-15 14:16:48 -04:00
|
|
|
buildingEdits: forwardPatch
|
2019-08-23 12:35:17 -04:00
|
|
|
});
|
2019-08-21 17:20:31 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle update directly
|
|
|
|
* - e.g. as callback from MultiTextInput where we set a list of strings
|
|
|
|
*
|
2019-08-23 12:46:22 -04:00
|
|
|
* @param {String} name
|
2019-08-21 17:20:31 -04:00
|
|
|
* @param {*} value
|
|
|
|
*/
|
2019-10-18 10:06:50 -04:00
|
|
|
handleChange(name: string, value: any) {
|
2019-08-23 12:35:17 -04:00
|
|
|
this.updateBuildingState(name, value);
|
2019-08-21 17:20:31 -04:00
|
|
|
}
|
|
|
|
|
2019-10-21 06:20:10 -04:00
|
|
|
handleReset() {
|
|
|
|
this.clearEdits();
|
|
|
|
}
|
|
|
|
|
2019-08-21 17:20:31 -04:00
|
|
|
/**
|
|
|
|
* Handle likes separately
|
|
|
|
* - like/love reaction is limited to set/unset per user
|
|
|
|
*
|
2019-08-23 12:35:17 -04:00
|
|
|
* @param {*} event
|
2019-08-21 17:20:31 -04:00
|
|
|
*/
|
2019-10-18 10:06:50 -04:00
|
|
|
async handleLike(like: boolean) {
|
|
|
|
try {
|
2020-01-02 05:59:13 -05:00
|
|
|
const data = await apiPost(
|
|
|
|
`/api/buildings/${this.props.building.building_id}/like.json`,
|
|
|
|
{like: like}
|
|
|
|
);
|
2020-08-04 10:54:49 -04:00
|
|
|
|
2019-10-18 10:06:50 -04:00
|
|
|
if (data.error) {
|
2019-11-07 03:13:30 -05:00
|
|
|
this.setState({error: data.error});
|
2019-08-21 17:20:31 -04:00
|
|
|
} else {
|
2021-02-22 01:59:24 -05:00
|
|
|
// like endpoint returns whole building data so we can update both
|
|
|
|
this.props.onBuildingUpdate(this.props.building.building_id, data);
|
|
|
|
this.props.onBuildingLikeUpdate(this.props.building.building_id, like);
|
2019-08-21 17:20:31 -04:00
|
|
|
}
|
2019-10-18 10:06:50 -04:00
|
|
|
} catch(err) {
|
|
|
|
this.setState({error: err});
|
|
|
|
}
|
2019-08-21 17:20:31 -04:00
|
|
|
}
|
|
|
|
|
2019-10-15 14:16:48 -04:00
|
|
|
async handleSubmit(event) {
|
2019-08-21 17:20:31 -04:00
|
|
|
event.preventDefault();
|
2019-10-15 14:16:48 -04:00
|
|
|
this.setState({error: undefined});
|
2019-08-21 17:20:31 -04:00
|
|
|
|
2019-10-15 14:16:48 -04:00
|
|
|
try {
|
2020-01-02 05:59:13 -05:00
|
|
|
const data = await apiPost(
|
|
|
|
`/api/buildings/${this.props.building.building_id}.json`,
|
|
|
|
this.state.buildingEdits
|
|
|
|
);
|
2020-08-04 10:54:49 -04:00
|
|
|
|
2019-10-15 14:16:48 -04:00
|
|
|
if (data.error) {
|
2019-11-07 03:13:30 -05:00
|
|
|
this.setState({error: data.error});
|
2019-08-21 17:20:31 -04:00
|
|
|
} else {
|
2021-02-22 01:59:24 -05:00
|
|
|
this.props.onBuildingUpdate(this.props.building.building_id, data);
|
2019-08-21 17:20:31 -04:00
|
|
|
}
|
2019-10-15 14:16:48 -04:00
|
|
|
} catch(err) {
|
|
|
|
this.setState({error: err});
|
|
|
|
}
|
2019-08-21 17:20:31 -04:00
|
|
|
}
|
|
|
|
|
2020-08-07 09:48:45 -04:00
|
|
|
async handleVerify(slug: string, verify: boolean, x: number, y: number) {
|
2020-08-04 10:54:49 -04:00
|
|
|
const verifyPatch = {};
|
|
|
|
if (verify) {
|
|
|
|
verifyPatch[slug] = this.props.building[slug];
|
|
|
|
} else {
|
|
|
|
verifyPatch[slug] = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
const data = await apiPost(
|
|
|
|
`/api/buildings/${this.props.building.building_id}/verify.json`,
|
|
|
|
verifyPatch
|
|
|
|
);
|
|
|
|
|
|
|
|
if (data.error) {
|
|
|
|
this.setState({error: data.error});
|
|
|
|
} else {
|
|
|
|
if (verify) {
|
2020-08-07 09:48:45 -04:00
|
|
|
Confetti({
|
|
|
|
angle: 60,
|
|
|
|
disableForReducedMotion: true,
|
|
|
|
origin: {x, y},
|
|
|
|
zIndex: 2000
|
|
|
|
});
|
2020-08-04 10:54:49 -04:00
|
|
|
}
|
2021-02-22 01:59:24 -05:00
|
|
|
this.props.onUserVerifiedUpdate(this.props.building.building_id, data);
|
2020-08-04 10:54:49 -04:00
|
|
|
}
|
|
|
|
} catch(err) {
|
|
|
|
this.setState({error: err});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-14 16:54:00 -04:00
|
|
|
render() {
|
2019-10-15 14:16:48 -04:00
|
|
|
const currentBuilding = this.getEditedBuilding();
|
|
|
|
|
2019-11-07 03:13:30 -05:00
|
|
|
const values_to_copy = {};
|
2019-08-21 17:20:31 -04:00
|
|
|
for (const key of Object.keys(this.state.keys_to_copy)) {
|
2019-11-07 03:13:30 -05:00
|
|
|
values_to_copy[key] = currentBuilding[key];
|
2019-08-21 17:20:31 -04:00
|
|
|
}
|
|
|
|
const data_string = JSON.stringify(values_to_copy);
|
2019-10-15 10:44:22 -04:00
|
|
|
const copy: CopyProps = {
|
2019-08-14 16:54:00 -04:00
|
|
|
copying: this.state.copying,
|
|
|
|
toggleCopying: this.toggleCopying,
|
|
|
|
toggleCopyAttribute: this.toggleCopyAttribute,
|
2019-10-15 10:44:22 -04:00
|
|
|
copyingKey: (key: string) => this.state.keys_to_copy[key]
|
2019-11-07 03:13:30 -05:00
|
|
|
};
|
2019-10-24 07:13:07 -04:00
|
|
|
|
|
|
|
const headerBackLink = `/${this.props.mode}/categories${this.props.building != undefined ? `/${this.props.building.building_id}` : ''}`;
|
2019-10-21 06:20:10 -04:00
|
|
|
const edited = this.isEdited();
|
2019-10-29 13:58:01 -04:00
|
|
|
|
2019-09-08 20:10:52 -04:00
|
|
|
return (
|
2019-08-21 17:20:31 -04:00
|
|
|
<section
|
2019-10-15 09:37:23 -04:00
|
|
|
id={this.props.cat}
|
2019-08-21 17:20:31 -04:00
|
|
|
className="data-section">
|
2019-08-23 12:35:17 -04:00
|
|
|
<ContainerHeader
|
2019-10-24 07:13:07 -04:00
|
|
|
cat={this.props.cat}
|
|
|
|
title={this.props.title}
|
|
|
|
>
|
|
|
|
{
|
|
|
|
this.props.help && !copy.copying?
|
|
|
|
<a
|
|
|
|
className="icon-button help"
|
|
|
|
title="Find out more"
|
|
|
|
href={this.props.help}>
|
|
|
|
Info
|
|
|
|
</a>
|
|
|
|
: null
|
|
|
|
}
|
|
|
|
{
|
|
|
|
this.props.building != undefined && !this.props.inactive ?
|
|
|
|
<>
|
|
|
|
<CopyControl
|
|
|
|
cat={this.props.cat}
|
|
|
|
data_string={data_string}
|
|
|
|
copying={copy.copying}
|
|
|
|
toggleCopying={copy.toggleCopying}
|
|
|
|
/>
|
|
|
|
{
|
|
|
|
!copy.copying ?
|
|
|
|
<>
|
|
|
|
<NavLink
|
|
|
|
className="icon-button history"
|
|
|
|
to={`/${this.props.mode}/${this.props.cat}/${this.props.building.building_id}/history`}
|
|
|
|
>History</NavLink>
|
|
|
|
<ViewEditControl
|
|
|
|
cat={this.props.cat}
|
|
|
|
mode={this.props.mode}
|
|
|
|
building={this.props.building}
|
|
|
|
/>
|
|
|
|
</>
|
|
|
|
:
|
|
|
|
null
|
|
|
|
}
|
|
|
|
</>
|
|
|
|
: null
|
|
|
|
}
|
|
|
|
</ContainerHeader>
|
2019-10-15 09:37:23 -04:00
|
|
|
<div className="section-body">
|
2019-09-08 20:10:52 -04:00
|
|
|
{
|
2019-10-15 09:37:23 -04:00
|
|
|
this.props.inactive ?
|
|
|
|
<Fragment>
|
2019-10-02 10:15:13 -04:00
|
|
|
<WrappedComponent
|
2019-10-18 10:06:50 -04:00
|
|
|
intro={this.props.intro}
|
2021-02-24 22:22:25 -05:00
|
|
|
building={this.props.building}
|
|
|
|
building_like={this.props.building_like}
|
2019-10-02 10:15:13 -04:00
|
|
|
mode={this.props.mode}
|
2020-08-04 14:23:07 -04:00
|
|
|
edited={false}
|
2019-10-02 10:15:13 -04:00
|
|
|
copy={copy}
|
2021-02-24 22:22:25 -05:00
|
|
|
onChange={undefined}
|
|
|
|
onLike={undefined}
|
|
|
|
onVerify={undefined}
|
2020-08-04 10:54:49 -04:00
|
|
|
user_verified={[]}
|
2019-10-02 10:15:13 -04:00
|
|
|
/>
|
2019-10-15 09:37:23 -04:00
|
|
|
</Fragment> :
|
|
|
|
this.props.building != undefined ?
|
|
|
|
<form
|
|
|
|
action={`/edit/${this.props.cat}/${this.props.building.building_id}`}
|
|
|
|
method="POST"
|
|
|
|
onSubmit={this.handleSubmit}>
|
|
|
|
{
|
|
|
|
(this.props.mode === 'edit' && !this.props.inactive) ?
|
|
|
|
<Fragment>
|
|
|
|
<ErrorBox msg={this.state.error} />
|
|
|
|
{
|
2019-10-21 06:20:10 -04:00
|
|
|
this.props.cat !== 'like' ? // special-case for likes
|
2019-10-15 09:37:23 -04:00
|
|
|
<div className="buttons-container with-space">
|
|
|
|
<button
|
|
|
|
type="submit"
|
2019-10-21 06:20:10 -04:00
|
|
|
className="btn btn-primary"
|
|
|
|
disabled={!edited}
|
|
|
|
aria-disabled={!edited}>
|
2019-10-15 09:37:23 -04:00
|
|
|
Save
|
|
|
|
</button>
|
2019-10-21 06:20:10 -04:00
|
|
|
{
|
|
|
|
edited ?
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
className="btn btn-warning"
|
|
|
|
onClick={this.handleReset}
|
|
|
|
>
|
|
|
|
Discard changes
|
|
|
|
</button> :
|
|
|
|
null
|
|
|
|
}
|
2019-10-15 14:16:48 -04:00
|
|
|
</div> :
|
|
|
|
null
|
2019-10-15 09:37:23 -04:00
|
|
|
}
|
|
|
|
</Fragment>
|
|
|
|
: null
|
|
|
|
}
|
|
|
|
<WrappedComponent
|
2019-10-18 10:06:50 -04:00
|
|
|
intro={this.props.intro}
|
2019-10-15 14:16:48 -04:00
|
|
|
building={currentBuilding}
|
2019-10-15 09:37:23 -04:00
|
|
|
building_like={this.props.building_like}
|
|
|
|
mode={this.props.mode}
|
2020-08-04 14:11:08 -04:00
|
|
|
edited={edited}
|
2019-10-15 09:37:23 -04:00
|
|
|
copy={copy}
|
|
|
|
onChange={this.handleChange}
|
|
|
|
onLike={this.handleLike}
|
2020-08-04 10:54:49 -04:00
|
|
|
onVerify={this.handleVerify}
|
|
|
|
user_verified={this.props.user_verified}
|
|
|
|
user={this.props.user}
|
2019-10-15 09:37:23 -04:00
|
|
|
/>
|
|
|
|
</form> :
|
2019-10-02 10:15:13 -04:00
|
|
|
<InfoBox msg="Select a building to view data"></InfoBox>
|
2019-09-08 20:10:52 -04:00
|
|
|
}
|
2019-10-15 09:37:23 -04:00
|
|
|
</div>
|
2019-08-21 17:20:31 -04:00
|
|
|
</section>
|
2019-09-08 20:10:52 -04:00
|
|
|
);
|
2019-08-14 16:54:00 -04:00
|
|
|
}
|
2019-11-07 03:13:30 -05:00
|
|
|
};
|
|
|
|
};
|
2019-08-14 16:54:00 -04:00
|
|
|
|
2019-11-05 15:13:10 -05:00
|
|
|
export default withCopyEdit;
|