Source: pages/UpdateExhibitPage.js

import React, {Component} from 'react';
import * as apiCalls from "../apiCalls/apiCalls";
import handleError from "../shared/failureHandler";
import Spinner from "../components/Spinner";
import Input from "../components/Input";
import ButtonWithProgress from "../components/ButtonWithProgress";
import {EXHIBITS_IMAGES_URL, INFO_LABELS_IMAGES_URL} from "../shared/sharedConstants";
import {CKEditor} from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
import LocationPicker from "../components/LocationPicker";
import PageContentContainer from "../components/PageContentContainer";
import BreadcrumbsLink from "../components/BreadcrumbsLink";
import Breadcrumbs from "../components/Breadcrumbs";
import PageInfo from "../components/PageInfo";

/**
 * page for updating exhibit
 */
class UpdateExhibitPage extends Component {

    /**
     * current page state
     */
    state = {
        exhibitId: 0,
        name: "",
        infoLabelText: "",
        buildingId: null,
        roomId: null,
        showcaseId: null,
        createdAt: "",
        image: "",
        encodedImage: null,
        imageSelect: "",
        infoLabel: null,
        encodedInfoLabel: null,
        infoLabelSelect: "",
        pendingApiCallGetInstitution: false,
        pendingApiCallUpdateInstitution: false,
        pendingApiCallUpdateImage: false,
        pendingApiCallUpdateInfoLabel: false,
        exhibitUpdated: false,
        imageUpdated: false,
        infoLabelUpdated: false,
        errors: {},
    }

    /**
     * called when page is mounted
     */
    componentDidMount() {
        this.setState({pendingApiCallGetInstitution: true})
        // get exhibit info from server
        apiCalls.getExhibit(this.props.match.params.exhibitId).then(response => {
            const buildingId = response.data.building ? response.data.building.buildingId : null;
            const roomId = response.data.room ? response.data.room.roomId : null;
            const showcaseId = response.data.showcase ? response.data.showcase.showcaseId : null;
            this.setState({...response.data, pendingApiCallGetInstitution: false, buildingId, roomId, showcaseId});
        }).catch(error => {
            // handle unauthenticated state
            return handleError(error);
        });
    }

    /**
     * called when image is selected
     * @param event input event
     * @param encoded encoded image prop name
     * @param updated updated prop name
     */
    onImageSelect = (event, encoded, updated) => {
        const errors = {...this.state.errors};
        delete errors[encoded];
        // update value in state and remove errors
        this.setState({errors, [event.target.name]: event.target.value, [updated]: false});
        if (event.target.files.length === 0) {
            return;
        }

        const file = event.target.files[0];
        let reader = new FileReader();
        reader.onloadend = () => {
            // set base64 encoded image to state
            this.setState({[encoded]: reader.result});
        }
        // read image
        reader.readAsDataURL(file);
    }

    /**
     * clears image from state
     * @param encode encoded image prop name
     * @param select select image prop name
     */
    clearImage = (encode, select) => {
        const errors = {...this.state.errors};
        delete errors[encode];
        // clear image fields and delete errors
        this.setState({errors, [encode]: null, [select]: ""});
    }

    /**
     * called when user submit exhibit image to be updated
     */
    onClickImageUpdate = (e) => {
        e.preventDefault();

        this.setState({pendingApiCallUpdateImage: true});
        const img = { encodedImage: this.state.encodedImage }

        // send request to server to update exhibit image
        apiCalls.updateExhibitImage(this.state.exhibitId, img).then(response => {
            this.setState({pendingApiCallUpdateImage: false, image: response.data.message, imageUpdated: true}, () => {
                this.clearImage("encodedImage", "imageSelect");
            });
        }).catch(error => {
            // handle unauthorized state
            return handleError(error);
        }).catch(apiError => {
            // handle user input errors
            this.handleApiError(apiError, "pendingApiCallUpdateImage");
        });
    }

    /**
     * handles error from http requests
     * @param apiError error from request
     * @param apiCall name of api call
     */
    handleApiError = (apiError, apiCall) => {
        if (apiError.response.data && apiError.response.data.validationErrors) {
            // define new page errors
            let errors = {
                ...this.state.errors,
                ...apiError.response.data.validationErrors
            };
            // set errors in state
            this.setState({
                [apiCall]: false,
                errors
            });
        }
    }

    /**
     * called when user submits new exhibit info label image
     */
    onClickInfoImageUpdate = () => {
        this.setState({pendingApiCallUpdateInfoLabel: true});
        const img = { encodedImage: this.state.encodedInfoLabel }

        // send updated info label to server
        apiCalls.updateExhibitInfoLabelImage(this.state.exhibitId, img).then(response => {
            this.setState({pendingApiCallUpdateInfoLabel: false, infoLabel: response.data.message, infoLabelUpdated: true}, () => {
                this.clearImage("encodedInfoLabel", "infoLabelSelect");
            });
        }).catch(error => {
            // handles unauthorized state
            return handleError(error);
        }).catch(apiError => {
            // handle error from user input
            if (apiError.response.data && apiError.response.data.validationErrors) {
                let errors = {
                    ...this.state.errors,
                };
                errors.encodedInfoLabel = apiError.response.data.validationErrors.encodedImage;
                this.setState({pendingApiCallUpdateInfoLabel: false, errors});
            }
        });
    }

    /**
     * called when text input is changed
     * @param event input event
     */
    onChange = (event) => {
        // set deleted errors and set value
        const errors = {...this.state.errors};
        delete errors[event.target.name];
        this.setState({errors, [event.target.name]: event.target.value, exhibitUpdated: false});
    }

    /**
     * called when value in info text editor is changed
     * @param event input event
     * @param editor ck editor
     */
    onInfoLabelTextChange = (event, editor) => {
        // change value and delete errors
        const errors = {...this.state.errors};
        delete errors["infoLabelText"];
        this.setState({infoLabelText: editor.getData(), exhibitUpdated: false, errors});
    }

    /**
     * called when user wants to update info abou exhibit
     */
    onClickExhibitUpdate = (e) => {
        e.preventDefault();

        this.setState({pendingApiCallUpdateInstitution: true});
        // extract exhibit from state
        const exhibit = {
            name: this.state.name,
            infoLabelText: this.state.infoLabelText,
            buildingId: this.state.buildingId,
            roomId: this.state.roomId,
            showcaseId: this.state.showcaseId,
        }

        // send update exhibit request to server
        apiCalls.updateExhibit(this.state.exhibitId, exhibit).then(response => {
            this.setState({exhibitUpdated: true, pendingApiCallUpdateInstitution: false});
        }).catch(error => {
            // handle unauthorized state
            return handleError(error);
        }).catch(apiError => {
            // handle error in user input
            this.handleApiError(apiError, "pendingApiCallUpdateInstitution");
        });
    }

    /**
     * updates new info about selected building, room and showcase
     * @param buildingId building id
     * @param roomId room id
     * @param showcaseId showcase id
     */
    updateLocation = (buildingId, roomId, showcaseId) => {
        this.setState({buildingId, roomId, showcaseId});
    }

    /**
     * renders exhibit update page
     * @returns {JSX.Element} page
     */
    render() {
        const {
            name,
            infoLabelText,
            buildingId,
            roomId,
            showcaseId,
            image,
            encodedImage,
            imageSelect,
            infoLabel,
            encodedInfoLabel,
            infoLabelSelect,
            pendingApiCallGetInstitution,
            pendingApiCallUpdateInstitution,
            pendingApiCallUpdateImage,
            pendingApiCallUpdateInfoLabel,
            exhibitUpdated,
            imageUpdated,
            infoLabelUpdated,
            errors,
        } = this.state;

        // define content
        let content = <Spinner />;
        if(!pendingApiCallGetInstitution) {
            content = (
                <div>
                    <form className="mb-4">
                        {
                            imageUpdated &&
                            <div className="alert alert-success" role="alert">
                                Exhibit image updated
                            </div>
                        }

                        <div className="form-group">
                            <Input type="file"
                                   onlyImage
                                   value={imageSelect}
                                   name="imageSelect"
                                   label="Exhibit image"
                                   placeholder="Select exhibit image"
                                   onChange={(event) => this.onImageSelect(event, "encodedImage", "imageUpdated")}
                                   hasError={errors.encodedImage && true}
                                   error={errors.encodedImage}
                            />

                            {
                                encodedImage ?
                                    <div>
                                        <img className="img-fluid sizedImg img-thumbnail mt-2" src={encodedImage} alt="upload" />
                                        <br />
                                        <button className="btn btn-danger btn-lg mt-2" disabled={pendingApiCallUpdateImage} onClick={() => this.clearImage("encodedImage", "imageSelect")}>
                                            <i className="fa fa-times" /> Clear
                                        </button>
                                        <ButtonWithProgress onClick={(e) => this.onClickImageUpdate(e)}
                                                            className="btn btn-success btn-lg ml-2 mt-2"
                                                            disabled={pendingApiCallUpdateImage}
                                                            pendingApiCall={pendingApiCallUpdateImage}
                                                            hasChildren>
                                            <i className="fa fa-paper-plane" /> Update image
                                        </ButtonWithProgress>
                                    </div>
                                    :
                                    <div>
                                        <img className="img-fluid sizedImg img-thumbnail mt-2" src={EXHIBITS_IMAGES_URL + image} alt="upload" />
                                    </div>
                            }
                        </div>
                    </form>

                    <form className="mb-4">
                        {
                            infoLabelUpdated &&
                            <div className="alert alert-success" role="alert">
                                Information label image updated
                            </div>
                        }

                        <div className="form-group">
                            <Input type="file"
                                   onlyImage
                                   value={infoLabelSelect}
                                   name="infoLabelSelect"
                                   label="Information label image"
                                   placeholder="Select information label image"
                                   onChange={(event) => this.onImageSelect(event, "encodedInfoLabel", "infoLabelUpdated")}
                                   hasError={errors.encodedInfoLabel && true}
                                   error={errors.encodedInfoLabel}
                            />

                            {
                                encodedInfoLabel ?
                                    <div>
                                        <img className="img-fluid mt-2" src={encodedInfoLabel} alt="upload" />
                                        <br />
                                        <button className="btn btn-danger btn-lg mt-2" disabled={pendingApiCallUpdateInfoLabel} onClick={() => this.clearImage("encodedInfoLabel", "infoLabelSelect")}>
                                            <i className="fa fa-times" /> Clear
                                        </button>
                                        <ButtonWithProgress onClick={(event) => {event.preventDefault(); this.onClickInfoImageUpdate();}}
                                                            className="btn btn-success btn-lg ml-2 mt-2"
                                                            disabled={pendingApiCallUpdateImage}
                                                            pendingApiCall={pendingApiCallUpdateImage}
                                                            hasChildren>
                                            <i className="fa fa-paper-plane" /> Update information label image
                                        </ButtonWithProgress>
                                    </div>
                                    :
                                    <div>
                                        <img className="img-fluid mt-2" src={INFO_LABELS_IMAGES_URL + infoLabel} alt="upload" />
                                    </div>
                            }
                        </div>
                    </form>

                    <form className="mb-4">
                        {
                            exhibitUpdated &&
                            <div className="alert alert-success" role="alert">
                                Exhibit information updated
                            </div>
                        }

                        <div className="form-group">
                            <Input
                                label="Name"
                                placeholder="Enter name" name="name" value={name}
                                onChange={this.onChange}
                                hasError={errors.name && true}
                                error={errors.name}
                            />
                        </div>
                        <div className="form-group">
                            <label className="font-weight-bold">Information label text</label>
                            <CKEditor editor={ClassicEditor}
                                      config={{removePlugins: ['Table', 'CKFinder', 'Link', 'TableToolbar', 'EasyImage', 'MediaEmbed', 'ImageCaption', 'ImageStyle', 'ImageToolbar', 'ImageUpload']}}
                                      data={infoLabelText} onChange={this.onInfoLabelTextChange}/>
                            {
                                errors.infoLabelText &&
                                <div className="text-danger">
                                    <small>{errors.infoLabelText}</small>
                                </div>
                            }
                        </div>
                        <div className="form-group">
                            <LocationPicker update={this.updateLocation} buildingId={buildingId} roomId={roomId} showcaseId={showcaseId} />
                        </div>

                        <ButtonWithProgress  onClick={(e) => this.onClickExhibitUpdate(e)}
                                             className="btn btn-primary w-100 my-2"
                                             disabled={pendingApiCallUpdateInstitution || name === ""}
                                             pendingApiCall={pendingApiCallUpdateInstitution}
                                             hasChildren>
                            <i className="fa fa-paper-plane" /> Update exhibit
                        </ButtonWithProgress>
                    </form>
                </div>
            )
        }

        // render page
        return (
            <PageContentContainer>
                <Breadcrumbs>
                    <BreadcrumbsLink to="/myInstitution" name="My Institution"/>
                    <BreadcrumbsLink to="/myInstitution/exhibits" name="Exhibits"/>
                    <li className="breadcrumb-item active">Exhibit</li>
                </Breadcrumbs>

                <PageInfo name="Exhibit">Here you can view and update the selected exhibit</PageInfo>
                {content}
            </PageContentContainer>
        );
    }
}

export default UpdateExhibitPage;