import React, { Fragment } from 'react'; import { NavLink, Redirect } from 'react-router-dom'; import Confetti from 'canvas-confetti'; import { apiPost } from '../apiHelpers'; import ErrorBox from '../components/error-box'; import InfoBox from '../components/info-box'; import { compareObjects } from '../helpers'; import { Building } from '../models/building'; import { User } from '../models/user'; import ContainerHeader from './container-header'; import { CategoryViewProps, CopyProps } from './data-containers/category-view-props'; import { CopyControl } from './header-buttons/copy-control'; import { ViewEditControl } from './header-buttons/view-edit-control'; interface DataContainerProps { title: string; cat: string; intro: string; help: string; inactive?: boolean; user?: User; mode: 'view' | 'edit'; building?: Building; building_like?: boolean; user_verified?: any; selectBuilding: (building: Building) => void; } interface DataContainerState { error: string; copying: boolean; keys_to_copy: {[key: string]: boolean}; currentBuildingId: number; currentBuildingRevisionId: number; buildingEdits: Partial; } /** * 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 */ const withCopyEdit = (WrappedComponent: React.ComponentType) => { return class DataContainer extends React.Component { constructor(props) { super(props); this.state = { error: undefined, copying: false, keys_to_copy: {}, buildingEdits: {}, currentBuildingId: undefined, currentBuildingRevisionId: undefined }; this.handleChange = this.handleChange.bind(this); this.handleReset = this.handleReset.bind(this); this.handleLike = this.handleLike.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.handleVerify = this.handleVerify.bind(this); this.toggleCopying = this.toggleCopying.bind(this); this.toggleCopyAttribute = this.toggleCopyAttribute.bind(this); } static getDerivedStateFromProps(props, state) { const newBuildingId = props.building == undefined ? undefined : props.building.building_id; const newBuildingRevisionId = props.building == undefined ? undefined : props.building.revision_id; if(newBuildingId !== state.currentBuildingId || newBuildingRevisionId > state.currentBuildingRevisionId) { return { error: undefined, copying: false, keys_to_copy: {}, buildingEdits: {}, currentBuildingId: newBuildingId, currentBuildingRevisionId: newBuildingRevisionId }; } return null; } /** * 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: string) { const keys = {...this.state.keys_to_copy}; if(this.state.keys_to_copy[key]){ delete keys[key]; } else { keys[key] = true; } this.setState({ keys_to_copy: keys }); } isEdited() { const edits = this.state.buildingEdits; // check if the edits object has any fields return Object.entries(edits).length !== 0; } clearEdits() { this.setState({ buildingEdits: {} }); } getEditedBuilding() { if(this.isEdited()) { return Object.assign({}, this.props.building, this.state.buildingEdits); } else { return {...this.props.building}; } } updateBuildingState(key: string, value: any) { const newBuilding = this.getEditedBuilding(); newBuilding[key] = value; const [forwardPatch] = compareObjects(this.props.building, newBuilding); this.setState({ buildingEdits: forwardPatch }); } /** * Handle update directly * - e.g. as callback from MultiTextInput where we set a list of strings * * @param {String} name * @param {*} value */ handleChange(name: string, value: any) { this.updateBuildingState(name, value); } handleReset() { this.clearEdits(); } /** * Handle likes separately * - like/love reaction is limited to set/unset per user * * @param {*} event */ async handleLike(like: boolean) { try { const data = await apiPost( `/api/buildings/${this.props.building.building_id}/like.json`, {like: like} ); if (data.error) { this.setState({error: data.error}); } else { this.props.selectBuilding(data); this.updateBuildingState('likes_total', data.likes_total); } } catch(err) { this.setState({error: err}); } } async handleSubmit(event) { event.preventDefault(); this.setState({error: undefined}); try { const data = await apiPost( `/api/buildings/${this.props.building.building_id}.json`, this.state.buildingEdits ); if (data.error) { this.setState({error: data.error}); } else { this.props.selectBuilding(data); } } catch(err) { this.setState({error: err}); } } async handleVerify(slug: string, verify: boolean, x: number, y: number) { 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) { Confetti({ angle: 60, disableForReducedMotion: true, origin: {x, y}, zIndex: 2000 }); } this.props.selectBuilding(this.props.building); } } catch(err) { this.setState({error: err}); } } render() { const currentBuilding = this.getEditedBuilding(); const values_to_copy = {}; for (const key of Object.keys(this.state.keys_to_copy)) { values_to_copy[key] = currentBuilding[key]; } const data_string = JSON.stringify(values_to_copy); const copy: CopyProps = { copying: this.state.copying, toggleCopying: this.toggleCopying, toggleCopyAttribute: this.toggleCopyAttribute, copyingKey: (key: string) => this.state.keys_to_copy[key] }; const headerBackLink = `/${this.props.mode}/categories${this.props.building != undefined ? `/${this.props.building.building_id}` : ''}`; const edited = this.isEdited(); return (
{ this.props.help && !copy.copying? Info : null } { this.props.building != undefined && !this.props.inactive ? <> { !copy.copying ? <> History : null } : null }
{ this.props.inactive ? : this.props.building != undefined ?
{ (this.props.mode === 'edit' && !this.props.inactive) ? { this.props.cat !== 'like' ? // special-case for likes
{ edited ? : null }
: null }
: null } : }
); } }; }; export default withCopyEdit;