Creating a PDF conversion tool using Serverless and Amplify - Part 2 - more React Components

JonthumbPosted by Jonathan Weyermann on December 6, 2019 at 12:00 AM
React

Here are there additional react components I never mentioned in Part 1 of this tutorial

<Image />

This represents a container for one of the up to  8 images being displayed on the screen at one time. On mount it looks for the presence of the image file. If the file is not there yet, it displays an animated grid loader. It re-checks for the presence of the zip file in increasingly larger intervals. It uses 'react-modal-image' to create a full screen modal of each image when clicked on. Here is the render method:

render () {
  return (
    <Col md={3}>
      <div className="image-background">
        { this.state.modelActive ? this.showModal() : null}
        { this.imagePlaceHolder() }
        <Button variant="secondary" href={this.props.meta} className="button-padding-less"><FontAwesomeIcon icon="download" /></Button>
      </div>
    </Col>
  )
}

At a high level, it consists of an imagePlaceHolder,  download button (with a fontawesome icon), and spot for the model when it's active


imagePlaceHolder = () => {
  if (this.state.loaded===2){
    return (
      <React.Fragment>
        { this.conditionalRender() }
      </React.Fragment>
    );
  } else {
    return (
      <div className='center'>
        <div className='block'>
          <GridLoader
            sizeUnit={"px"}
            size={35}
            color={'#123abc'}
            loading={this.state.loading}
          />
        </div>
      </div>
    )
  }
}

The imagePlaceHolder can be either show a GridLoader (from 'react-spinners/GridLoader') or the conditionalRender method, which containers a ModalImage (from "react-modal-image") and opens the clicked on image in a nice prebuild modal

conditionalRender = () => {
  return (
    <ModalImage
      small={this.props.meta}
      large={this.props.meta}
      showRotate={true}
    />
  )
}


On mount, this component waits for the image to be loaded, polling the location of the image in ever increasing intervals until a timeout is reached

componentDidMount = async () => {
  this.setState({name: this.props.meta})  
  var buffer=1000
    while (FileStatus(this.props.meta)===403) {
      await this.sleep(buffer);
      buffer = buffer * 1.1
      this.setState({loaded: 1})
      if (buffer >= 15000) {
        return null;
      }
    }
  this.setState({loaded: 2})
}

sleep = (ms) => {
  return new Promise(resolve => setTimeout(resolve, ms));
}

For the full code for image.js, see the github repository.


<PdfImages />

This is mostly a wrapper around the pdf images that implements the pagination to allow me to limit the number of images to 8 a page. It uses react-paginate. The render method either returns a loadingBar, or set of image components, wrapped in a paginate object

render() {
  return (
    <Container>
      { this.props.data.images.length===0 ? <LoadingBar /> : this.allImages() }
    </Container>
  )
}

allImages() returns the configured react-paginate component and an image method (which wraps <Image> and gives it proper data)

allImages = () => {
  return (
    <div>
      <div>
        <h6 className='mt2 center'>{this.props.data.uploadFileName}.pdf: Page {this.props.data.firstPage} to {this.props.data.lastPage} of {this.props.data.numPages}</h6>
      </div>
      <Row>
        { this.images() }
      </Row>
      <Row>
        <ReactPaginate
          previousLabel={'previous'}
          nextLabel={'next'}
          breakLabel={'...'}
          breakClassName={'break-me'}
          pageCount={this.props.data.numPages/pagesToDisplay}
          marginPagesDisplayed={2}
          pageRangeDisplayed={pagesToDisplay}
          onPageChange={this.handlePageClick}
          containerClassName={'pagination'}
          subContainerClassName={'pages'}
          activeClassName={'active'}
        />
      </Row>
    </div>
  )
}

images = () => {
  let imageData = this.props.data.images.map((image, i) => {
    return (<Image meta={image} key={i} />)
  })
  return (
    <React.Fragment>
      {imageData}
    </React.Fragment>
  );
}


<S3Upload />

This component uploads the pdf to s3 using axios. First, it makes a signed POST request to the api endpoint generated by amplify. The signed request can only be made from the url the app is running on. The signed request is then used to upload the pdf to s3 using the user specific naming convention using an axios PUT request. While these requests are happening, a <LoadingBar /> is displayed.

import React, {Component} from 'react'
import axios from 'axios'
import LoadingBar from '../components/loadingBar'
import { pagesToDisplay } from '../constants'

class S3Upload extends Component {
  s3Upload = () => {
    axios.post(process.env.REACT_APP_API_URL,{
      fileName : this.props.data.s3SafeFileName,
      fileType : this.props.data.uploadFileType
    })
    .then(response => {
      var returnData = response.data.body;
      var signedRequest = returnData.signedRequest;
      console.log("Recieved a signed request " + signedRequest);
      var options = {
        headers: {
          'Content-Type': `application/${this.props.data.uploadFileType}`
        }
      };
      axios.put(signedRequest,this.props.data.uploadFile,options)
      .then(result => {
        var imgs = []
        console.log("Response from s3: Number of Pages now:", this.props.data.numPages);
        for(var index=1;index <= Math.min(pagesToDisplay,this.props.data.numPages) ;index++){
          imgs.push(`${process.env.REACT_APP_IMAGE_BUCKET}${this.props.data.s3SafeFileName.split('.')[0]}/image${index}.jpg`);
        }
        this.props.setToSuccess(imgs)
      })
      .catch(error => {
        console.log("ERROR " + JSON.stringify(error));
      })
    })
    .catch(error => {
      console.log(JSON.stringify(error));
    })
    return ( <LoadingBar /> )
  }

  render() {
    return (
      <React.Fragment>
        {this.s3Upload()}
      </React.Fragment>
    )
  }
}

export default S3Upload;



<Zip />

Components surrounding the zip button. On mount it looks for the presence of the zip file. If the file is not there yet, it displays an animated beat loader loading bar. On mount it checks for the presence of the zip file in increasingly larger intervals.

componentDidMount = async () => {
  var buffer=1000
  while (FileStatus(this.props.fileName)===403) {
    await this.sleep(buffer);
    buffer = buffer * 1.1
    this.setState({loaded: 1})
    if (buffer >= 15000) {
      return null;
    }
  }
  this.props.addToPreviousUploads()
  this.setState({loaded: 2})
}

sleep = (ms) => {
  return new Promise(resolve => setTimeout(resolve, ms));
}


The zipDownload method either return a button (if the zip file has been generated), or a BeatLoader (a type of animated loader)

zipDownload = () => {
  if (this.state.loaded===2){
    return (
      <React.Fragment>
        <Button variant="secondary" href={this.props.fileName} className="button-padding"><span className='large-image'><FontAwesomeIcon icon="download" /></span>All Images(Zip File)</Button>
      </React.Fragment>
    );
  } else {
    return (
      <React.Fragment>
        <Button variant="secondary" className="button-padding">Creating your zip file..
          <BeatLoader
            sizeUnit={"px"}
            size={25}
            color={'#123abc'}
            loading={this.state.loading}
          />
        </Button>
      </React.Fragment>
    )
  }
}

render () {
  return (
    <React.Fragment>
      { this.zipDownload() }
    </React.Fragment>
  )
}

The full code is available here. You will also require loadingBar and all the css files to make it look like how it looks at https://pdf2pdfs.com 

This is by no means an exhaustive list of components one can extract. Further refactoring out of components will depend on where you want to take the project. 


Deployment

In order to deploy the app we also have to add hosting with amplify.

amplify add hosting

You can follow the instructions. I tend to use both s3 and cloudfront. I find it's probably best to create both even in development mode, as Amplify will typically duplicate all your resources in any environment you create. Amplify environments are outside the scope of this tutorial, but you can read more about them by typing

amplify env help


we're ready to deploy the app. To create our amplify resources in the cloud, and deploy our code to the cloud in one command, we can use

amplify publish

Amplify will prompt you for confirmation and then create all your resources and deploy your app to s3 and cloudfront. You will get a cloudfront link to where your app is deployed. You could then purchase your own domain with Route 53 and point it to your cloudfront url. Instructions on how to do this can be found here.

This concludes part 2 of the PDF2JPG tutorial. We still need to build our backend lambda to do the pdf conversion itself. Instructions can be found in part 3.

Add Comment