Categories
JavaScript React

Add Captchas to a React App with reaptcha

Captchas stands for ‘Completely Automated Public Turing test to tell Computers and Humans Apart’. It’s often used to prevent automated traffic from tampering with websites.

With React, we can easily add a captcha to our app by using the reaptcha package. It’s very easy to use.

Installation

We can install it by running:

npm install --save reaptcha

We can also use yarn to install it y running:

yarn add reaptcha

Basic Usage

We can use it by signing up for a Recaptcha API key so that we can use Google’s Recaptcha service to use component. It works by injecting the Recaptcha scripts into our React app.

To sign up for an API key pair, go to https://www.google.com/recaptcha/ to sign up for an API key for free.

When we sign up for a key, we should sign up for a V2 Recaptcha key and add the domain that we want our Recaptcha widget to work on by entering the domain without any ports or child paths.

Once we signed up for an API key pair, we click Copy Site Key to copy the key that’s used on client side.

Next, we create our React app project and then write the following code:

import React from "react";
import Reaptcha from "reaptcha";

export default function App() {
  const [verified, setVerified] = React.useState(false);

  const onVerify = e => {
    setVerified(true);
  };

  return (
    <div className="App">
      <form>
        <Reaptcha
          sitekey="your_key"
          onVerify={onVerify}
        />
        <button type="submit" disabled={!verified}>
          Submit
        </button>
      </form>
    </div>
  );
}

In the code above, we put in our key as the value of siteKey. Then we have the onVerify callback, which is called when the captcha displayed is verified.

The onVerifed function is the callback that’s called when the captcha is successfully verified. Therefore, we use call setVerified there to enable the Submit button.

If we get an error, check if our domain is entered corrected in the API key settings. Also, we must be using the V2 version of reCAPTCHA with this package.

Once we did that, we should see a captcha, which enables the Submit button when we verified the displayed captcha.

Loading Captcha Manually

Reaptcha can also load captchas only when the user explicity does something to make it load.

We have to call the captcha’s renderExplicitly method to load the captcha. For instance, we can do that as follows:

import React from "react";
import Reaptcha from "reaptcha";

export default function App() {
  const [verified, setVerified] = React.useState(false);
  const [captchaReady, setCaptchaReady] = React.useState(false);
  const captcha = React.createRef();
  const onVerify = e => {
    setVerified(true);
  };

  const onLoad = () => {
    setCaptchaReady(true);
  };

  return (
    <div className="App">
      <button
        disabled={!captchaReady}
        onClick={() => {
          captcha.current.renderExplicitly();
        }}
      >
        Render reCAPTCHA
      </button>
      <form>
        <Reaptcha
          onLoad={onLoad}
          sitekey="your_key"
          onVerify={onVerify}
          ref={captcha}
          explicit
        />
        <button type="submit" disabled={!verified}>
          Submit
        </button>
      </form>
    </div>
  );
}

In the code above, we have a button to load the captcha when it’s clicked. It’s only enabled with then captcha script is finished loading so we can call the renderExplicitly method to load it.

In the Reaptcha component, we added the onLoad prop that calls the onLoad method. which calls setCaptchaReady to set the captchaReady property totrue` to enable the button.

Once is captcha is ready and the user clicks the Render reCAPTCHA button, we call captcha.current.renderExplicitly(); where captcha is the ref that we referenced in Reaptcha.

Most importantly, we added the explicit prop so that the captcha has to be loaded with an explicit user action.

Invisible Captchas

Reaptcha also supports invisible captchas. We can set the size prop to be invisible to make an invisible captcha.

For instance, we can create an invisible captcha as follows:

import React from "react";
import Reaptcha from "reaptcha";

export default function App() {
  const [verified, setVerified] = React.useState(false);
  const captcha = React.createRef();
  const onVerify = e => {
    setVerified(true);
  };

  return (
    <div className="App">
      <button
        onClick={() => {
          captcha.current.execute();
        }}
      >
        Execute reCAPTCHA
      </button>
      <form>
        <Reaptcha
          sitekey="your_key"
          onVerify={onVerify}
          ref={captcha}
          size="invisible"
        />
        <button type="submit" disabled={!verified}>
          Submit
        </button>
      </form>
    </div>
  );
}

We have to create a key for the V2 Invisible Captcha so that we can incorporate invisible captchas into our app.

Then we call captcha.current.execute(); where captcha is the ref for the Reaptcha component.

Methods

We can call the reset method on the Reaptcha ref to reset the reCAPTCHA instance and getResponse methos to return the response from reCAPTCHA. They both return promises.

Other Options

Other options that we can pass into the Reaptcha component as props include:

  • tabindex – HTML tab index
  • inject – boolean to indicate whether we want to inject the captcha script to the DOM automatically.
  • isolated – boolean to indicate that this component shouldn’t interfere with existing reCAPTCHA installations on a page
  • onError – error handler
  • children – a function to access instance methods without the need to use refs.

Conclusion

Reaptcha is an easy to use React component for incorporating the V2 reCAPTCHA script into our app. All we have to do is to add the component, sign up for the reCAPTCHA API key and then set a few options to get the captcha added to our React app.

Categories
JavaScript React

Add a PDF Reader to a React App with react-pdf-viewer

Adding React viewers is a common requirement for web apps. With React, there’s the react-pdf-viewer package that lets us add PDF viewers to React apps easily.

In this article, we’ll look at how to use it to add a PDF viewer to our React app.

Installation

We can install it by running:

npm install @phuocng/react-pdf-viewer

Basic Usage

After installing it, we can use it as follows:

import React from "react";
import Viewer, { Worker } from "@phuocng/react-pdf-viewer";

import "@phuocng/react-pdf-viewer/cjs/react-pdf-viewer.css";

export default function App() {
  return (
    <div className="App">
      <Worker workerUrl="https://unpkg.com/pdfjs-dist@2.2.228/build/pdf.worker.min.js">
        <div style={{ height: "750px" }}>
          <Viewer fileUrl="dummy.pdf" />
        </div>
      </Worker>
    </div>
  );
}

In the code above, we included the CSS file that comes with the package, and the Viewer for opening the PDF viewer and the Worker component for loading the PDF specified in the fileUrl prop as in the background.

We have to include the workerUrl prop with that URL so that the worker in that location is run.

Then we’ll get a PDF viewer that has zoom in and out controls, page navigation, document properties, and download options.

In a Create React App project, static files like PDFs should be in the public folder so that it can be loaded. Also, PDFs have to be in the same domain as the React app so that we won’t get CORS errors.

Options

There’re options for customization. We can change the layout of the sidebar, toolbar, and replace default controls with React components of our choice.

Layout options available include:

  • isSidebarOpened – boolean to indicate whether we want the sidebar to open or not
  • main – the main part of the viewer (a Slot component object)
  • toolbar – toolbar part (a RenderToolbar component object)
  • sidebarSlot object to define the sidebar of the viewer

For instance, we can write the following code to add a sidebar to display pages and a layout component do define a layout for our PDF viewer as follows:

import React from "react";
import Viewer, {
  Worker,
} from "@phuocng/react-pdf-viewer";

import "@phuocng/react-pdf-viewer/cjs/react-pdf-viewer.css";


export default function App() {
  const renderToolbar = (toolbarSlot) => {
    return (
      <div
        style={{
          alignItems: 'center',
          display: 'flex',
          width: '100%',
        }}
      >
        <div
          style={{
            alignItems: 'center',
            display: 'flex',
          }}
        >
          <div style={{ padding: '0 5px' }}>
            {toolbarSlot.searchPopover}
          </div>
          <div style={{ padding: '0 5px' }}>
            {toolbarSlot.previousPageButton}
          </div>
          <div style={{ padding: '0 5px' }}>
            {toolbarSlot.currentPageInput} / {toolbarSlot.numPages}
          </div>
          <div style={{ padding: '0 5px' }}>
            {toolbarSlot.nextPageButton}
          </div>
        </div>
        <div
          style={{
            alignItems: 'center',
            display: 'flex',
            flexGrow: 1,
            flexShrink: 1,
            justifyContent: 'center',
          }}
        >
          <div style={{ padding: '0 5px' }}>
            {toolbarSlot.zoomOutButton}
          </div>
          <div style={{ padding: '0 5px' }}>
            {toolbarSlot.zoomPopover}
          </div>
          <div style={{ padding: '0 5px' }}>
            {toolbarSlot.zoomInButton}
          </div>
        </div>
        <div
          style={{
            alignItems: 'center',
            display: 'flex',
            marginLeft: 'auto',
          }}
        >
          <div style={{ padding: '0 5px' }}>
            {toolbarSlot.fullScreenButton}
          </div>
          <div style={{ padding: '0 5px' }}>
            {toolbarSlot.openFileButton}
          </div>
          <div style={{ padding: '0 5px' }}>
            {toolbarSlot.downloadButton}
          </div>
          <div style={{ padding: '0 5px' }}>
            {toolbarSlot.moreActionsPopover}
          </div>
        </div>
      </div>
    );
  };

  const layout = (
    isSidebarOpened,
    main,
    toolbar,
    sidebar
  ) => {
    return (
      <div
        style={{
          border: '1px solid rgba(0, 0, 0, .3)',
          display: 'grid',
          gridTemplateAreas: "'toolbar toolbar' 'sidebar main'",
          gridTemplateColumns: '30% 1fr',
          gridTemplateRows: '40px calc(100% - 40px)',
          height: '100%',
          overflow: 'hidden',
          width: '100%',
        }}
      >
        <div
          style={{
            alignItems: 'center',
            backgroundColor: '#EEE',
            borderBottom: '1px solid rgba(0, 0, 0, .1)',
            display: 'flex',
            gridArea: 'toolbar',
            justifyContent: 'center',
            padding: '4px',
          }}
        >
          {toolbar(renderToolbar)}
        </div>
        <div
          style={{
            borderRight: '1px solid rgba(0, 0, 0, 0.2)',
            display: 'flex',
            gridArea: 'sidebar',
            justifyContent: 'center',
          }}
        >
          {sidebar.children}
        </div>
        <div
          {...main.attrs}
          style={Object.assign({}, {
            gridArea: 'main',
            overflow: 'scroll',
          }, main.attrs.style)}
        >
          {main.children}
        </div>
      </div>
    );
  };

  return (
    <Worker workerUrl="https://unpkg.com/pdfjs-dist@2.2.228/build/pdf.worker.min.js">
      <Viewer
        fileUrl='dummy.pdf'
        layout={layout}
      />
    </Worker>
  );
}

The default layout is the following:

┌───────────┬───────────┐
│ toolbar   │ toolbar   │
├───────────┼───────────┤
│ sidebar   │ main      │
└───────────┴───────────┘

In the code above, we reference those parts in the places we wish to place in the layout component. The children attribute has the parts of each component. attrs has the default props of each component, which we can change.

The renderToolbar function is a higher-order component that takes a toolbarSlot prop, which has the parts of the toolbar and we can place them accordingly according to our needs. In the example above, we put them in different divs and added our own styles to each div.

The grid above is a CSS grid, so we can modify the layout as we please and it’ll will in all modern browsers.

Those pats are passed in as props in the layout component. So we can reference them directly from the parameters of layout.

Conclusion

The react-pdf-viewer package is a very useful PDF viewer that’s designed with both performance and usability in mind. The default layout and controls are already very good. Performance comes from loading PDFs in the background with a web worker.

It’s also very customizable, we can define a layout component that has toolbar, sidebar and main as props and then we can customize them as we wish.

Categories
JavaScript React

Use React to Display Images in a Grid Like Google and Flickr

If you use image search websites like Google Image Search or Flickr, you will notice that their images display in a grid that looks like a wall of bricks. The images are uneven in height, but equal in width. This is called the masonry effect because it looks like a wall of bricks.

To implement the masonry effect, we have to set the width of the image proportional to the screen width and set the image height to be proportional to the aspect ratio of the image.

This is a pain to do if it’s done without any libraries, so people have made libraries to create this effect.

In this article, we will build a photo app that allows users to search for images and display images in a masonry grid. The image grid will have infinite scroll to get more images. We will build it with React and the React Masonry Component library. For infinite scrolling, we will use the React Infinite Scroller library. We will wrap the React Infinite Scroller outside the React Masonry Component to get infinite scrolling with the masonry effect when displaying images.

Our app will display images from the Pixabay API. You can view the API documentation and register for a key at https://pixabay.com/api/docs/

To start, we run Create React App to create the app. Run npx create-react-app photo-app to create the initial code for the app.

Then we install our own libraries. We need React Infinite Scroller, React Masonry Component, Bootstrap for styling, Axios for making HTTP requests, Formik and Yup for form value data binding and form validation, and React Router for routing URLs to our pages.

To install all the packages, run:

npm i axios bootstrap formik react-bootstrap react-infinite-scroller react-masonry-component react-router-dom yup

to install all the packages.

With all the packages installed, we can start building the app. First start with replacing the code in App.js with:

import React from "react";  
import { Router, Route } from "react-router-dom";  
import HomePage from "./HomePage";  
import { createBrowserHistory as createHistory } from "history";  
import TopBar from "./TopBar";  
import ImageSearchPage from "./ImageSearchPage";  
import "./App.css";  
const history = createHistory();

function App() {  
  return (  
    <div className="App">  
      <Router history={history}>  
        <TopBar />  
        <Route path="/" exact component={HomePage} />  
        <Route path="/imagesearch" exact component={ImageSearchPage} />  
      </Router>  
    </div>  
  );  
}

export default App;

to add the top bar and the routes for our app into the entry point of the app.

Next remove all the code in App.css and add:

.page {  
  padding: 20px;  
}

to add padding to our pages.

Then we set our React Masonry Component options by creating a exports.js in the src folder and add:

export const masonryOptions = {  
  fitWidth: true,  
  columnWidth: 300,  
  gutter: 5  
};

These options are very important. We need to set fitWidth to true to center our grid. columnWidth must be a number to get constant width. It will scale according to screen size only with constant width. The gutter value is the margin between items.

The full list of options are at https://masonry.desandro.com/options.html

Next we create our app’s home page by creating HomePage.js in the src folder and add:

import React from "react";  
import { getImages } from "./request";  
import InfiniteScroll from "react-infinite-scroller";  
import Masonry from "react-masonry-component";  
import "./HomePage.css";  
import { masonryOptions } from "./exports";

function HomePage() {  
  const [images, setImages] = React.useState([]);  
  const [page, setPage] = React.useState(1);  
  const [total, setTotal] = React.useState(0);  
  const [initialized, setInitialized] = React.useState(false); 
  const getAllImages = async (pg = 1) => {  
    const response = await getImages(page);  
    let imgs = images.concat(response.data.hits);  
    setImages(imgs);  
    setTotal(response.data.total);  
    pg++;  
    setPage(pg);  
  }; 

  React.useEffect(() => {  
    if (!initialized) {  
      getAllImages();  
      setInitialized(true);  
    }  
  }); 

  return (  
    <div className="page">  
      <h1 className="text-center">Home</h1>  
      <InfiniteScroll  
        pageStart={1}  
        loadMore={getAllImages}  
        hasMore={total > images.length}  
      >  
        <Masonry  
          className={"grid"}  
          elementType={"div"}  
          options={masonryOptions}  
          disableImagesLoaded={false}  
          updateOnEachImageLoad={false}  
        >  
          {images.map((img, i) => {  
            return (  
              <div key={i}>  
                <img src={img.previewURL} style={{ width: 300 }} />  
              </div>  
            );  
          })}  
        </Masonry>  
      </InfiniteScroll>  
    </div>  
  );  
}  
export default HomePage;

In the home page, we just get the images when the page loads. When the user scroll down, we load more images by adding 1 to the currentpage value and get the image with the page number.

With the InfiniteScroll component, which is provided by React Infinite Scroll, wrapped outside the Masonry component, which is provided by React Masonry Component, we display our images in a grid, and also display more when the user scrolls down until the length of the images array is greater than or equal to the total, which is from the total field given by the Pixabay API’s results.

We load images when the page loads by checking if initialized flag is true, we only load images on page load if initialized is false and the when the request is first made to the API and succeeds, then we set initialized flag to true to stop requests from being made on every render.

Next we create a image search page by creating the ImageSearchPage.js file and adding the following:

import React from "react";  
import { Formik } from "formik";  
import Form from "react-bootstrap/Form";  
import Col from "react-bootstrap/Col";  
import Button from "react-bootstrap/Button";  
import * as yup from "yup";  
import InfiniteScroll from "react-infinite-scroller";  
import Masonry from "react-masonry-component";  
import { masonryOptions } from "./exports";  
import { searchImages } from "./request";

const schema = yup.object({  
  keyword: yup.string().required("Keyword is required")  
});

function ImageSearchPage() {  
  const [images, setImages] = React.useState([]);  
  const [keyword, setKeyword] = React.useState([]);  
  const [page, setPage] = React.useState(1);  
  const [total, setTotal] = React.useState(0);  
  const [searching, setSearching] = React.useState(false); 
  
  const handleSubmit = async evt => {  
    const isValid = await schema.validate(evt);  
    if (!isValid) {  
      return;  
    }  
    setKeyword(evt.keyword);  
    searchAllImages(evt.keyword, 1);  
  }; 

  const searchAllImages = async (keyword, pg = 1) => {  
    setSearching(true); 
    const response = await searchImages(keyword, page);  
    let imgs = response.data.hits;  
    setImages(imgs);  
    setTotal(response.data.total);  
    setPage(pg);  
  }; 

  const getMoreImages = async () => {  
    let pg = page;  
    pg++;  
    const response = await searchImages(keyword, pg);  
    const imgs = images.concat(response.data.hits);  
    setImages(imgs);  
    setTotal(response.data.total);  
    setPage(pg);  
  }; 

  React.useEffect(() => {}); 
  
  return (  
    <div className="page">  
      <h1 className="text-center">Search</h1>  
      <Formik validationSchema={schema} onSubmit={handleSubmit}>  
        {({  
          handleSubmit,  
          handleChange,  
          handleBlur,  
          values,  
          touched,  
          isInvalid,  
          errors  
        }) => (  
          <Form noValidate onSubmit={handleSubmit}>  
            <Form.Row>  
              <Form.Group as={Col} md="12" controlId="keyword">  
                <Form.Label>Keyword</Form.Label>  
                <Form.Control  
                  type="text"  
                  name="keyword"  
                  placeholder="Keyword"  
                  value={values.keyword || ""}  
                  onChange={handleChange}  
                  isInvalid={touched.keyword && errors.keyword}  
                />  
                <Form.Control.Feedback type="invalid">  
                  {errors.keyword}  
                </Form.Control.Feedback>  
              </Form.Group>  
            </Form.Row>  
            <Button type="submit" style={{ marginRight: "10px" }}>  
              Search  
            </Button>  
          </Form>  
        )}  
      </Formik>  
      <br />  
      <InfiniteScroll  
        pageStart={1}  
        loadMore={getMoreImages}  
        hasMore={searching && total > images.length}  
      >  
        <Masonry  
          className={"grid"}  
          elementType={"div"}  
          options={masonryOptions}  
          disableImagesLoaded={false}  
          updateOnEachImageLoad={false}  
        >  
          {images.map((img, i) => {  
            return (  
              <div key={i}>  
                <img src={img.previewURL} style={{ width: 300 }} />  
              </div>  
            );  
          })}  
        </Masonry>  
      </InfiniteScroll>  
    </div>  
  );  
}  
export default ImageSearchPage;

We do not load images on the first load on this page. Instead, the user enters a search term in the form and when the user clicks the Search button, then handleSubmit is called. The evt object has the form values, which is updated by the Formik component. Yup provides the form validation object with the schema object, we just check if keyword is required.

In the handlesubmit function, we get the evt object, which we validate against the schema by callingschema.validate, which returns a promise. If the promise returns to something truthy, then we proceed with making the request to the Pixabay API with the search keyword and page number.

We have the same setup as the home page for the infinite scroll and masonry effect image grid. The only difference is that we call the searchAllImages function which has similar logic as the getAllImages function, except that we pass in the keyword parameter in addition to the page parameter. We set the imgs variable to the array returned from the Pixabay API and set the images by calling setImages. We also set the page by calling setPage.

When the user scrolls far enough down that content runs out, the getMoreImages function is called when images.length is less than the total. The total is set by getting the total field from the API.

We use masonryOptions from exports.js just like in the home page and display images the same way.

Next create request.js in the src folder to add the code for making HTTP requests to the back end, like so:

const axios = require("axios");  
const APIURL = "https://pixabay.com/api";

export const getImages = (page = 1) =>  
  axios.get(`${APIURL}/?page=${page}&key=${process.env.REACT_APP_APIKEY}`);

export const searchImages = (keyword, page = 1) =>  
  axios.get(  
    `${APIURL}/?page=${page}&key=${process.env.REACT_APP_APIKEY}&q=${keyword}`  
  );

We have the getImages for just getting images and searchImages that also sends the search term to the API. process.env.REACT_APP_APIKEY is from setting the REACT_APP_APIKEY variable in the .env file in the project’s root folder.

Next create TopBar.js in the src folder and add:

import React from "react";  
import Navbar from "react-bootstrap/Navbar";  
import Nav from "react-bootstrap/Nav";  
import { withRouter } from "react-router-dom";

function TopBar({ location }) {  
  React.useEffect(() => {}); 

  return (  
    <Navbar bg="primary" expand="lg" variant="dark">  
      <Navbar.Brand href="#home">Photo App</Navbar.Brand>  
      <Navbar.Toggle aria-controls="basic-navbar-nav" />  
      <Navbar.Collapse id="basic-navbar-nav">  
        <Nav className="mr-auto">  
          <Nav.Link href="/" active={location.pathname == "/"}>  
            Home  
          </Nav.Link>  
          <Nav.Link  
            href="/imagesearch"  
            active={location.pathname == "/imagesearch"}  
          >  
            Search  
          </Nav.Link>  
        </Nav>  
      </Navbar.Collapse>  
    </Navbar>  
  );  
}  

export default withRouter(TopBar);

This contains the React Bootstrap Navbar to show a top bar with a link to the home page and the name of the app. We check the location.pathname to highlight the right links by setting the active prop, where the location prop is provided by React Router by wrapping the withRouter function outside the TopBar component.

Finally, in index.js , we replace the existing code with:

<!DOCTYPE html>  
<html lang="en">  
  <head>  
    <meta charset="utf-8" />  
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />  
    <meta name="viewport" content="width=device-width, initial-scale=1" />  
    <meta name="theme-color" content="#000000" />  
    <meta  
      name="description"  
      content="Web site created using create-react-app"  
    />  
    <link rel="apple-touch-icon" href="logo192.png" />  
    <!--  
      manifest.json provides metadata used when your web app is installed on a  
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ 
    -->  
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />  
    <!--  
      Notice the use of %PUBLIC_URL% in the tags above.  
      It will be replaced with the URL of the `public` folder during the build.  
      Only files inside the `public` folder can be referenced from the HTML.Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC\_URL%/favicon.ico" will  
      work correctly both with client-side routing and a non-root public URL.  
      Learn how to configure a non-root public URL by running `npm run build`.  
    -->  
    <title>Photo App</title>  
    <link  
      rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"  
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"  
      crossorigin="anonymous"  
    />  
  </head>  
  <body>  
    <noscript>You need to enable JavaScript to run this app.</noscript>  
    <div id="root"></div>  
    <!--  
      This HTML file is a template.  
      If you open it directly in the browser, you will see an empty page. You can add webfonts, meta tags, or analytics to this file.  
      The build step will place the bundled scripts into the <body> tag. To begin the development, run `npm start` or `yarn start`.  
      To create a production bundle, use `npm run build` or `yarn build`.  
    -->  
  </body>  
</html>

to add the Bootstrap CSS and change the title.

Categories
JavaScript React

How to Build Ionic Apps with React

With the latest version of the Ionic framework, a tool that lets you write mobile apps with web technologies, you can use React to build your mobile application. Before v4, Ionic only provided components for Angular, but now React has complete support for the components.

Ionic 4 is a mobile app framework and a component library. You can build mobile apps, progressive web apps, and normal web apps. The component library can also be used on its own. Of course, the hardware support remains in place for you to use if you wish.

The full reference for the React version of Ionic is at https://ionicframework.com/docs.

In this article, we will build a React web app with Ionic that does currency conversion. The home page will display the list of the latest exchange rates and another page will have a form to convert the currencies of your choice. To get the data, we use the Foreign exchange rates API located at https://exchangeratesapi.io/. We use the Open Exchange Rates API located at https://openexchangerates.org/ to get the list of currencies.

Getting Started

To start, we will install the Ionic CLI. We run:

npm install -g ionic@latest

Next, we run:

ionic start currency-converter --type=react

Then we select the Sidenav option to create an Ionic project with a left sidebar.

We will also need to install some other packages. We need Axios to make HTTP requests and MobX for state management. Run npm i axios mobx mobx-react in our project folder to install them.

Now we are ready to create some pages. In the pages folder, create ConvertCurrencyPage.jsx and add:

import {
  IonButtons,
  IonContent,
  IonHeader,
  IonItem,
  IonList,
  IonMenuButton,
  IonPage,
  IonTitle,
  IonToolbar,
  IonInput,
  IonLabel,
  IonSelect,
  IonSelectOption,
  IonButton
} from "@ionic/react";
import React from "react";
import { observer } from "mobx-react";
import { getExchangeRates } from "../requests";
const ConvertCurrencyPage = ({ currenciesStore }) => {
  const [fromCurrencies, setFromCurrencies] = React.useState({});
  const [toCurrencies, setToCurrencies] = React.useState({});
  const [values, setValues] = React.useState({ amount: 0 } as any);
  const [submitting, setSubmitting] = React.useState(false);
  const [toAmount, setToAmount] = React.useState(0);
const convertCurrency = async () => {
    setSubmitting(true);
    if (values.amount <= 0 || !values.from || !values.to) {
      return;
    }
    const { data } = await getExchangeRates(values.from);
    const rate = data.rates[values.to];
    setToAmount(values.amount * rate);
  };

  React.useEffect(() => {
    const fromCurrencies = {};
    for (let key in currenciesStore.currencies) {
      if (key != values.to) {
        fromCurrencies[key] = currenciesStore.currencies[key];
      }
    }
    setFromCurrencies(fromCurrencies);

  const toCurrencies = {};
    for (let key in currenciesStore.currencies) {
      if (key != values.from) {
        toCurrencies[key] = currenciesStore.currencies[key];
      }
    }
    setToCurrencies(toCurrencies);
  }, [currenciesStore.currencies, values.from, values.to]);

  return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonButtons slot="start">
            <IonMenuButton />
          </IonButtons>
          <IonTitle>Convert Currency</IonTitle>
        </IonToolbar>
      </IonHeader>
      <IonContent>
        <IonList lines="none">
          <IonItem>
            <IonInput
              type="number"
              value={values.amount}
              color={!values.amount && submitting ? "danger" : undefined}
              min="0"
              onIonChange={ev =>
                setValues({ ...values, amount: (ev.target as any).value })
              }
            ></IonInput>
          </IonItem>
          <IonItem>
            <IonLabel>Currency to Convert From</IonLabel>
            <IonSelect
              placeholder="Select One"
              color={!values.from && submitting ? "danger" : undefined}
              onIonChange={ev =>
                setValues({ ...values, from: (ev.target as any).value })
              }
            >
              {Object.keys(fromCurrencies).map(key => {
                return (
                  <IonSelectOption value={key} key={key}>
                    {(fromCurrencies as any)[key]}
                  </IonSelectOption>
                );
              })}
            </IonSelect>
          </IonItem>
          <IonItem>
            <IonLabel>Currency to Convert To</IonLabel>
            <IonSelect
              placeholder="Select One"
              color={!values.to && submitting ? "danger" : undefined}
              onIonChange={ev =>
                setValues({ ...values, to: (ev.target as any).value })
              }
            >
              {Object.keys(toCurrencies).map(key => {
                return (
                  <IonSelectOption value={key} key={key}>
                    {(toCurrencies as any)[key]}
                  </IonSelectOption>
                );
              })}
            </IonSelect>
          </IonItem>
          <IonItem>
            <IonButton size="default" fill="solid" onClick={convertCurrency}>
              Convert
            </IonButton>
          </IonItem>
          {toAmount ? (
            <IonItem>
              {values.amount} {values.from} is {toAmount} {values.to}.
            </IonItem>
          ) : (
            undefined
          )}
        </IonList>
      </IonContent>
    </IonPage>
  );
};
export default observer(ConvertCurrencyPage);

This adds a form to convert currency from one to another. In this form, we filter by choices by excluding from the currency being converted to from the first drop down and exclude the currency being converted from in the second dropdown. Also, we have an ion-input for the amount being converted. We get the values of the currency from the currenciesStore, which is the MobX store that gets the list of currencies from. In the IonSelect components we set the onIonChange props to the handler functions that set the drop down’s values. We also set the placeholder for all the inputs and selects. In the IonInput component, we do the same with the onIonChange handler. We show the correct value by using the variables set in the values object during change events.

When the user clicks Convert, we run the convertCurrency function. We check if the values are set correctly before running the rest of the code. If that succeeds, then we run the getExchangeRates function imported from requests.js, then we set the final toAmount by multiplying the rate with the amount.

The useEffect callback is used for excluding the currency to convert from the currency to convert to list and vice versa. The array in the second argument useEffect specifies which values to watch for.

The observer function in the last line is for designating the component to watch the latest values from the MobX stores.

Next in Home.tsx, we replace the existing code with:

import {
  IonButtons,
  IonContent,
  IonHeader,
  IonItem,
  IonLabel,
  IonList,
  IonListHeader,
  IonMenuButton,
  IonPage,
  IonTitle,
  IonToolbar,
  IonSelect,
  IonSelectOption
} from "@ionic/react";
import React from "react";
import "./Home.css";
import { getExchangeRates } from "../requests";
import { CurrencyStore } from "../stores";
import { observer } from "mobx-react";
const HomePage = ({ currencyStore, currenciesStore }) => {
  const [rates, setRates] = React.useState({});
  const getRates = async () => {
    const { data } = await getExchangeRates(
      (currencyStore as CurrencyStore).currency
    );
    setRates(data.rates);
  };

  React.useEffect(() => {
    getRates();
  }, [(currencyStore as CurrencyStore).currency]);
  return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonButtons slot="start">
            <IonMenuButton />
          </IonButtons>
          <IonTitle>Home</IonTitle>
        </IonToolbar>
      </IonHeader>
      <IonContent>
        <IonList lines="none">
          <IonListHeader>Latest Exchange Rates</IonListHeader>
          <IonItem>
            <IonLabel>Currency</IonLabel>
            <IonSelect
              placeholder="Select One"
              onIonChange={ev => {
                (currencyStore as CurrencyStore).setCurrency(
                  ev.target && (ev.target as any).value
                );
              }}
            >
              {Object.keys(currenciesStore.currencies).map(key => {
                return (
                  <IonSelectOption
                    value={key}
                    key={key}
                    selected={
                      (currencyStore as CurrencyStore).currency
                        ? key == (currencyStore as CurrencyStore).currency
                        : key == "AUD"
                    }
                  >
                    {(currenciesStore.currencies as any)[key]}
                  </IonSelectOption>
                );
              })}
            </IonSelect>
          </IonItem>
        </IonList>
        <IonList lines="none">
          <IonListHeader>Exchange Rates</IonListHeader>
          {Object.keys(rates).map(key => {
            console.log(rates);
            return (
              <IonItem>
                {key}: {rates[key]}
              </IonItem>
            );
          })}
        </IonList>
      </IonContent>
    </IonPage>
  );
};
export default observer(HomePage);

In this file, we display the exchange rates from an API. We get the currencies from the currenciesStore so that users can see exchange rates based on different currencies. The items are displayed in an IonList provided by Ionic.

The observer function in the last line is for designating the component to watches the latest values from the MobX stores.

Next in App.tsx, replace the following code with:

import React from "react";
import { Redirect, Route } from "react-router-dom";
import { IonApp, IonRouterOutlet, IonSplitPane } from "@ionic/react";
import { IonReactRouter } from "@ionic/react-router";
import { AppPage } from "./declarations";
import Menu from "./components/Menu";
import Home from "./pages/Home";
import { home, cash } from "ionicons/icons";
/* Core CSS required for Ionic components to work properly */
import "@ionic/react/css/core.css";
/* Basic CSS for apps built with Ionic */
import "@ionic/react/css/normalize.css";
import "@ionic/react/css/structure.css";
import "@ionic/react/css/typography.css";
/* Optional CSS utils that can be commented out */
import "@ionic/react/css/padding.css";
import "@ionic/react/css/float-elements.css";
import "@ionic/react/css/text-alignment.css";
import "@ionic/react/css/text-transformation.css";
import "@ionic/react/css/flex-utils.css";
import "@ionic/react/css/display.css";
/* Theme variables */
import "./theme/variables.css";
import ConvertCurrencyPage from "./pages/ConvertCurrencyPage";
import { CurrencyStore, CurrenciesStore } from "./stores";
import { getCurrenciesList } from "./requests";
const currencyStore = new CurrencyStore();
const currenciesStore = new CurrenciesStore();
const appPages: AppPage[] = [
  {
    title: "Home",
    url: "/home",
    icon: home
  },
  {
    title: "Convert Currency",
    url: "/convertcurrency",
    icon: cash
  }
];
const App: React.FC = () => {
  const [initialized, setInitialized] = React.useState(false);
  const getCurrencies = async () => {
    const { data } = await getCurrenciesList();
    currenciesStore.setCurrencies(data);
    setInitialized(true);
  };
  React.useEffect(() => {
    if (!initialized) {
      getCurrencies();
    }
  });
  return (
    <IonApp>
      <IonReactRouter>
        <IonSplitPane contentId="main">
          <Menu appPages={appPages} />
          <IonRouterOutlet id="main">
            <Route
              path="/home"
              render={() => (
                <Home
                  currencyStore={currencyStore}
                  currenciesStore={currenciesStore}
                />
              )}
              exact={true}
            />
            <Route
              path="/convertcurrency"
              render={() => (
                <ConvertCurrencyPage
                  currenciesStore={currenciesStore}
                />
              )}
              exact={true}
            />
            <Route exact path="/" render={() => <Redirect to="/home" />} />
          </IonRouterOutlet>
        </IonSplitPane>
      </IonReactRouter>
    </IonApp>
  );
};
export default App;

We modified the routes so that we use the render prop instead of the component prop since we want to pass in our MobX stores into the components. The stores contain the currently selected currency for the home page and the list of currencies for both pages.

We get/set the list of currencies here for both components.

Next, create requests.ts in the src folder and add:

const axios = require('axios');
const APIURL = 'https://api.exchangeratesapi.io';
const OPEN_EXCHANGE_RATES_URL = 'http://openexchangerates.org/api/currencies.json';

export const getExchangeRates = (baseCurrency: string) => axios.get(`${APIURL}/latest?base=${baseCurrency}`)
export const getCurrenciesList = () => axios.get(OPEN_EXCHANGE_RATES_URL)

This the code for making the HTTP requests to get the currencies and exchange rates.

Next create store.ts and add:

import { observable, action } from "mobx";
class CurrencyStore {
    @observable currency: string = 'AUD';
    @action setCurrency(currency: string) {
        this.currency = currency;
    }
}
class CurrenciesStore {
    @observable currencies = {};
    @action setCurrencies(currencies) {
        this.currencies = currencies;
    }
}
export { CurrencyStore, CurrenciesStore };

The CurrencyStore is for storing the selected currency in the home page and to show the exchange rates based on the selected currency. currency is the value observed by the home page and setCurrency sets the currency value. Similarly, CurrenciesStore stores a list of currencies retrieved in App.tsx where setCurrencies is called.

Next in tsconfig.json, we replace the existing code with:

{
  "compilerOptions": {
      "experimentalDecorators": true,
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "noImplicitAny": false
  },
  "include": [
    "src"
  ]
}

This changes noImplicitAny and sets it to true.

Categories
JavaScript React

How to Make a Calendar App with React

For many applications, recording dates is an important feature. Having a calendar is often a handy feature to have. Fortunately, many developers have made calendar components that other developers can easily add to their apps.

React has many calendar widgets that we can add to our apps. One of them is React Big Calendar. It has a lot of features. It has a month, week, and day calendar. Also, you can navigate easily to today or any other days with back and next buttons. You can also drag over a date range in the calendar to select the date range. With that, you can do any manipulation you want with the dates.

In this article, we will make a simple calendar app where users can drag over a date range and add a calendar entry. Users can also click on an existing calendar entry and edit the entry. Existing entries can also be deleted. The form for adding and editing the calendar entry will have date and time pickers to select the date and time.

We will save the data on the back end in a JSON file.

We will use React to build our app. To start, we run:

npx create-react-app calendar-app

to create the project.

Next we have to install a few packages. We will use Axios for HTTP requests to our back end, Bootstrap for styling, MobX for simple state management, React Big Calendar for our calendar component, React Datepicker for the date and time picker in our form , and React Router for routing.

To install them, we run:

npm i axios bootstrap mobx mobx-react moment react-big-calendar react-bootstrap react-datepicker react-router-dom

With all the packages installed, we can start writing the code. First, we replace the existing code in App.js with:

import React from "react";
import { Router, Route } from "react-router-dom";
import HomePage from "./HomePage";
import { createBrowserHistory as createHistory } from "history";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import "./App.css";
import "react-big-calendar/lib/css/react-big-calendar.css";
import "react-datepicker/dist/react-datepicker.css";
const history = createHistory();
function App({ calendarStore }) {
  return (
    <div>
      <Router history={history}>
        <Navbar bg="primary" expand="lg" variant="dark">
          <Navbar.Brand href="#home">Calendar App</Navbar.Brand>
          <Navbar.Toggle aria-controls="basic-navbar-nav" />
          <Navbar.Collapse id="basic-navbar-nav">
            <Nav className="mr-auto">
              <Nav.Link href="/">Home</Nav.Link>
            </Nav>
          </Navbar.Collapse>
        </Navbar>
        <Route
          path="/"
          exact
          component={props => (
            <HomePage {...props} calendarStore={calendarStore} />
          )}
        />
      </Router>
    </div>
  );
}
export default App;

We add the React Bootstrap top bar in here with a link to the home page. Also, we add the route for the home page in here with the MobX calendarStore passed in.

Also, we import the styles for the date picker and calendar here so that we can use them throughout the app.

Next in App.css , replace the existing code with:

.page {
  padding: 20px;
}
.form-control.react-datepicker-ignore-onclickoutside,
.react-datepicker-wrapper {
  width: 465px !important;
}
.react-datepicker__current-month,
.react-datepicker-time__header,
.react-datepicker-year-header,
.react-datepicker__day-name,
.react-datepicker__day,
[class^="react-datepicker__day--*"],
.react-datepicker__time-list-item {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
    "Droid Sans", "Helvetica Neue", sans-serif;
}

to add some padding to our page, change the width of the datepicker input and change the font of the datepicker.

Next create a file called CalendarForm.js in the src folder and add:

import React from "react";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import DatePicker from "react-datepicker";
import Button from "react-bootstrap/Button";
import {
  addCalendar,
  editCalendar,
  getCalendar,
  deleteCalendar
} from "./requests";
import { observer } from "mobx-react";
const buttonStyle = { marginRight: 10 };
function CalendarForm({ calendarStore, calendarEvent, onCancel, edit }) {
  const [start, setStart] = React.useState(null);
  const [end, setEnd] = React.useState(null);
  const [title, setTitle] = React.useState("");
  const [id, setId] = React.useState(null);
React.useEffect(() => {
    setTitle(calendarEvent.title);
    setStart(calendarEvent.start);
    setEnd(calendarEvent.end);
    setId(calendarEvent.id);
  }, [
    calendarEvent.title,
    calendarEvent.start,
    calendarEvent.end,
    calendarEvent.id
  ]);
const handleSubmit = async ev => {
    ev.preventDefault();
    if (!title || !start || !end) {
      return;
    }
if (+start > +end) {
      alert("Start date must be earlier than end date");
      return;
    }
    const data = { id, title, start, end };
    if (!edit) {
      await addCalendar(data);
    } else {
      await editCalendar(data);
    }
    const response = await getCalendar();
    const evs = response.data.map(d => {
      return {
        ...d,
        start: new Date(d.start),
        end: new Date(d.end)
      };
    });
    calendarStore.setCalendarEvents(evs);
    onCancel();
  };
  const handleStartChange = date => setStart(date);
  const handleEndChange = date => setEnd(date);
  const handleTitleChange = ev => setTitle(ev.target.value);
const deleteCalendarEvent = async () => {
    await deleteCalendar(calendarEvent.id);
    const response = await getCalendar();
    const evs = response.data.map(d => {
      return {
        ...d,
        start: new Date(d.start),
        end: new Date(d.end)
      };
    });
    calendarStore.setCalendarEvents(evs);
    onCancel();
  };
return (
    <Form noValidate onSubmit={handleSubmit}>
      <Form.Row>
        <Form.Group as={Col} md="12" controlId="title">
          <Form.Label>Title</Form.Label>
          <Form.Control
            type="text"
            name="title"
            placeholder="Title"
            value={title || ""}
            onChange={handleTitleChange}
            isInvalid={!title}
          />
          <Form.Control.Feedback type="invalid">{!title}</Form.Control.Feedback>
        </Form.Group>
      </Form.Row>
<Form.Row>
        <Form.Group as={Col} md="12" controlId="start">
          <Form.Label>Start</Form.Label>
          <br />
          <DatePicker
            showTimeSelect
            className="form-control"
            selected={start}
            onChange={handleStartChange}
          />
        </Form.Group>
      </Form.Row>
<Form.Row>
        <Form.Group as={Col} md="12" controlId="end">
          <Form.Label>End</Form.Label>
          <br />
          <DatePicker
            showTimeSelect
            className="form-control"
            selected={end}
            onChange={handleEndChange}
          />
        </Form.Group>
      </Form.Row>
      <Button type="submit" style={buttonStyle}>
        Save
      </Button>
      <Button type="button" style={buttonStyle} onClick={deleteCalendarEvent}>
        Delete
      </Button>
      <Button type="button" onClick={onCancel}>
        Cancel
      </Button>
    </Form>
  );
}
export default observer(CalendarForm);

This is the form for adding and editing the calendar entries. We add the React Bootstrap form in here by adding the Form component. The Form.Control is also from the same library. We use it for the title text input.

The other 2 fields are the start and end dates. We use React Datepicker in here to let use select the start and end dates of a calendar entry. In addition, we enable the time picker to let users pick the time.

There are change handlers in each field to update the values in the state so users can see what they entered and let them submit the data later. The change handlers are handleStartChange , handleEndChange and handleTitleChange . We set the states with the setter functions generated by the useState hooks.

We use the useEffect callback to set the fields in the calendarEvent prop to the states. We pass all the fields we want to set to the array in the second argument of the useEffect function so that the states will be updated whenever the latest value of the calendarEvent prop is passed in.

In the handleSubmit function, which is called when the form Save button is clicked. we have to call ev.preventDefault so that we can use Ajax to submit our form data.

If data validation passes, then we submit the data and get the latest and store them in our calendarStore MobX store.

We wrap observer outside the CalendarForm component so that we always get the latest values from calendarStore .

Next we create our home page. Create a HomePage.js file in the src folder and add:

import React from "react";
import { Calendar, momentLocalizer } from "react-big-calendar";
import moment from "moment";
import Modal from "react-bootstrap/Modal";
import CalendarForm from "./CalendarForm";
import { observer } from "mobx-react";
import { getCalendar } from "./requests";
const localizer = momentLocalizer(moment);
function HomePage({ calendarStore }) {
  const [showAddModal, setShowAddModal] = React.useState(false);
  const [showEditModal, setShowEditModal] = React.useState(false);
  const [calendarEvent, setCalendarEvent] = React.useState({});
  const [initialized, setInitialized] = React.useState(false);
  const hideModals = () => {
    setShowAddModal(false);
    setShowEditModal(false);
  };
  const getCalendarEvents = async () => {
    const response = await getCalendar();
    const evs = response.data.map(d => {
      return {
        ...d,
        start: new Date(d.start),
        end: new Date(d.end)
      };
    });
    calendarStore.setCalendarEvents(evs);
    setInitialized(true);
  };
  const handleSelect = (event, e) => {
    const { start, end } = event;
    const data = { title: "", start, end, allDay: false };
    setShowAddModal(true);
    setShowEditModal(false);
    setCalendarEvent(data);
  };
  const handleSelectEvent = (event, e) => {
    setShowAddModal(false);
    setShowEditModal(true);
    let { id, title, start, end, allDay } = event;
    start = new Date(start);
    end = new Date(end);
    const data = { id, title, start, end, allDay };
    setCalendarEvent(data);
  };
  React.useEffect(() => {
    if (!initialized) {
      getCalendarEvents();
    }
  });
  return (
    <div className="page">
      <Modal show={showAddModal} onHide={hideModals}>
        <Modal.Header closeButton>
          <Modal.Title>Add Calendar Event</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <CalendarForm
            calendarStore={calendarStore}
            calendarEvent={calendarEvent}
            onCancel={hideModals.bind(this)}
            edit={false}
          />
        </Modal.Body>
      </Modal>
      <Modal show={showEditModal} onHide={hideModals}>
        <Modal.Header closeButton>
          <Modal.Title>Edit Calendar Event</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <CalendarForm
            calendarStore={calendarStore}
            calendarEvent={calendarEvent}
            onCancel={hideModals.bind(this)}
            edit={true}
          />
        </Modal.Body>
      </Modal>
      <Calendar
        localizer={localizer}
        events={calendarStore.calendarEvents}
        startAccessor="start"
        endAccessor="end"
        selectable={true}
        style={{ height: "70vh" }}
        onSelectSlot={handleSelect}
        onSelectEvent={handleSelectEvent}
      />
    </div>
  );
}
export default observer(HomePage);

We get the calendar entries and populate them in the calendar here. The entries are retrieved from back end and then saved into the store. In the useEffect callback, we set get the items when the page loads. We only do it when initialized is false so we won’t be reloading the data every time the page renders.

To open the modal for adding calendar entries, we set the onSelectSlot prop with our handler so that we can call setShowAddModal and setCalendarEvent to set open the modal and set the dates before opening the add calendar event modal.

Similarly, we set the onSelectEvent modal with the handleSelectEvent handler function so that we open the edit modal and set the calendar event data of the existing entry.

Each Modal have the CalendarForm component inside. We pass in the function for closing the modals into the form so that we can close them from the form. Also, we pass in the calendarStore and calendarEvent so that they can be manipulated in the CalendarForm.

We wrap observer outside the CalendarForm component so that we always get the latest values from calendarStore .

Next in index.js , we replace the existing code with:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { CalendarStore } from "./store";
const calendarStore = new CalendarStore();
ReactDOM.render(
  <App calendarStore={calendarStore} />,
  document.getElementById("root")
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

so that we can pass in the MobX calendarStore into the root App component.

Next create a requests.js file in the src folder and add:

const APIURL = "http://localhost:3000";
const axios = require("axios");
export const getCalendar = () => axios.get(`${APIURL}/calendar`);
export const addCalendar = data => axios.post(`${APIURL}/calendar`, data);
export const editCalendar = data =>
  axios.put(`${APIURL}/calendar/${data.id}`, data);
export const deleteCalendar = id => axios.delete(`${APIURL}/calendar/${id}`);

These are the functions for making the HTTP calls to manipulate the calendar entries.

Next createstore.js in the src folder and add:

import { observable, action, decorate } from "mobx";
class CalendarStore {
  calendarEvents = [];
setCalendarEvents(calendarEvents) {
    this.calendarEvents = calendarEvents;
  }
}
CalendarStore = decorate(CalendarStore, {
  calendarEvents: observable,
  setCalendarEvents: action
});
export { CalendarStore };

to save the items in the store for access by all of our components.

Next in index.html , replace the existing code with:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>Calendar App</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

to add the Bootstrap CSS and rename the title.

Now all the hard work is done. All we have to do is use JSON Server NPM package located at https://github.com/typicode/json-server for our back end.

Install it by running:

npm i -g json-server

Then run it by running:

json-server --watch db.json

In db.json , replace the existing content with:

{  
  "calendar": []  
}

Next we run our app by running npm start in our app’s project folder and when the program asks you to run in a different port, select yes.