Create a Book Rating App with Strapi Ratings Plugin and Cloudinary
Published: 2023-01-10 17:25:06

Without a doubt, books are great. However, in some cases, we would like to give a review of books we have read. This is very important as we do not want to waste time on books that do not capture our interest.

In this tutorial, we will look at how to create a Book Rating application. It will introduce us to Cloudinary, Next.js, implementing a WYSIWYG editor, Strapi custom controllers, middleware, and policies.

# Prerequisites

Before we start we need to equip ourselves with the following:

1. NodeJs installed on our local machine. See [t](https://codex.so/how-to-install-the-latest-node-js-npm-and-yarn)[utorial](http://) for installation guide.
2. Basic understanding of Strapi - [get started](https://strapi.io/documentation/developer-docs/latest/getting-started/introduction.html) with this quick guide.
3. Basic knowledge of Next.js
4. Basic knowledge of Tailwind CSS



>[Github URL](https://github.com/Theodore-Kelechukwu-Onyejiaku/book-ratings-with-strapijs)

# What is NextJs?

A fantastic React framework for creating extremely dynamic applications is Next.js. Pre-rendering, server-side rendering, automatic code splitting, and many more fantastic features are included right out of the box.


# What is Tailwind CSS?

An efficient CSS framework for creating unique user interfaces is called Tailwind CSS. We directly write our CSS in our HTML classes while using Tailwind CSS. This is quite helpful as it eliminates the need to use a separate library or import an external stylesheet for UI designs.

# What is Strapi - A Headless CMS?

Strapi is an open-source headless CMS based on Node.js that is used to develop and manage content using Restful APIs and GraphQL.

With Strapi, we can scaffold our API faster and consume the content via APIs using any HTTP client or GraphQL enabled frontend.

# Scaffolding a Strapi Project

To scaffold a new Strapi project is very simple and works precisely as installing a new frontend framework.

We are going to start by running the following commands and testing them out in our default browser.

```
    npx create-strapi-app strapi-book-ratings --quickstart
    # OR
    yarn create strapi-app strapi-book-ratings --quick start
```
The command above will scaffold a new Strapi project in the directory you specified.

Next, run `yarn build` to build your app and `yarn develop` to run the new project if it doesn't start automatically. 

The last command will open a new tab with a page to register your new admin of the system. Go ahead and fill out the form and click on the submit button to create a new Admin.


# Installing Strapi Ratings Plugin

This plugin will allow us to add reviews to our application through some API routes.
Run the command below to install:

```
    npm install strapi-plugin-ratings
```
After installation, you should see a new tab for ratings plugin.

![Strapi Ratings Plugin Initial View](https://paper-attachments.dropboxusercontent.com/s_159DA53009009462465057F85C8E330FEFD1B79EFE91E697856995F539FCBE6A_1668614697510_ratings+plugin+intial+view.png)

# Building the Book collection

Next, we will create a new Collection Type that will store the details of each book.

For this reason, we will create the fields:
`title`, `info`, `creator`, `imageUrl` and `likes`.


![Creating a Book Collection Type](https://paper-attachments.dropboxusercontent.com/s_159DA53009009462465057F85C8E330FEFD1B79EFE91E697856995F539FCBE6A_1668613514764_Screen+Shot+2022-11-16+at+4.43.59+PM.png)


Click “continue”.  This would open up a screen to select fields. For the `title`, `info` , `creator` and `imageUrl` we would choose the `Text` field. And for the `likes` field, we will select 


![Creating the Title Field](https://paper-attachments.dropboxusercontent.com/s_159DA53009009462465057F85C8E330FEFD1B79EFE91E697856995F539FCBE6A_1668613789827_Screen+Shot+2022-11-16+at+4.48.14+PM.png)


Next, click on the “Advanced settings” tab to make sure that this field is “Required field”. This is so that the field will be required when creating a record.


![Making a Field Required](https://paper-attachments.dropboxusercontent.com/s_159DA53009009462465057F85C8E330FEFD1B79EFE91E697856995F539FCBE6A_1668614073142_Screen+Shot+2022-11-16+at+4.53.53+PM.png)


When all is set and done we should have the following fields:

| Field Name | Field Type | Required | Unique |
| ---------- | ---------- | -------- | ------ |
| title      | Short text | true     | false  |
| info       | Short text | true     | false  |
| creator    | Short text | true     | false  |
| imageUrl   | Short text | true     | false  |
| likes      | JSON       | false    | false  |



# Extending The User Collection Type

In the User collection type, add a Boolean field `isAdmin`. This will allow us to add a policy that will check if a user is an admin for any request to delete a book.


![](https://paper-attachments.dropboxusercontent.com/s_159DA53009009462465057F85C8E330FEFD1B79EFE91E697856995F539FCBE6A_1668619819559_Screen+Shot+2022-11-16+at+6.28.59+PM.png)


Ensure that this new field has a default value `false`.


![](https://paper-attachments.dropboxusercontent.com/s_159DA53009009462465057F85C8E330FEFD1B79EFE91E697856995F539FCBE6A_1668619892510_Screen+Shot+2022-11-16+at+6.31.14+PM.png)

# Allowing Public Access

When we interact with an API, there are cases whereby it is restricted, accessible, or limited to some requests or route actions. For route actions or requests that should be public, go to *Settings > Users & Permissions Plugin > Roles,* then click on Public. We would allow access to `find` , and `findOne` for Book collection. 


![Allowing Public Access to Book Collection](https://paper-attachments.dropboxusercontent.com/s_159DA53009009462465057F85C8E330FEFD1B79EFE91E697856995F539FCBE6A_1668657530889_Screen+Shot+2022-11-17+at+4.53.01+AM.png)


And allow `count`, `find` , `getPageSize`, `getStats` for Ratings Plugin. This is so that we can get the total number of ratings, find a ratings for a particular book, and get the rating statistics of a book without being logged in.


![Allowing Public Access to  Ratings Plugin](https://paper-attachments.dropboxusercontent.com/s_159DA53009009462465057F85C8E330FEFD1B79EFE91E697856995F539FCBE6A_1668657583752_Screen+Shot+2022-11-17+at+4.59.16+AM.png)



Next, we allow some route actions to authenticated users. We proceed by clicking the “Back” button, then the *Authenticated Role.* Now select the following for Book collection.


![Allowing access to Authenticated Users](https://paper-attachments.dropboxusercontent.com/s_159DA53009009462465057F85C8E330FEFD1B79EFE91E697856995F539FCBE6A_1668658172693_Screen+Shot+2022-11-17+at+5.09.12+AM.png)


Next, we allow access to authenticated users to create a review, find a review, and get users’ reviews for a particular book.


![Allow Access to Authenticated Users to Create Ratings](https://paper-attachments.dropboxusercontent.com/s_159DA53009009462465057F85C8E330FEFD1B79EFE91E697856995F539FCBE6A_1668658289257_Screen+Shot+2022-11-17+at+5.11.14+AM.png)



# Creating a Policy 

Policies are executed before a middleware and a controller. Our Book Ratings application demands that only an admin can delete a book. Remember we added an additional boolean field `isAdmin` to the `user` collection. This would help us differentiate between a user and an admin. 

Head over to the folder `src/api/book` and create a folder called `policies`. Inside it, create a file called `is-admin.js`. Next, add the following code to it:

```
    // path: ./src/api/book/policies/is-admin.js
    
    module.exports = async (policyContext, config, { strapi }) => {
        // check if user is admin
        if (policyContext.state.user.isAdmin) {
            // Go to controller's action.
            return true;
        }
        // if not admin block request
        return false;
    };
```

- line 5: This checks if the user making the request is an admin. By default, the `isAdmin` is `false` for every user. However, an admin will have a `true` value for the `isAdmin` field. 
- line 7: we Strapi to allow the request to head over to any controller that needs this policy.
- line 10: if the user is not an admin, we want to prevent or block the user from performing the request, in this case deleting a book.


# Adding Policy to Delete Book Route

Next, we want to add this policy to the `delete` route of our application. We need to modify routes logic to achieve this. Head on to the file `src/api/book/routes/book.js` and add the following:  

```
    // path: ./src/api/book/routes/book.js
    
    'use strict';
    /**
     * book router
     */
    const { createCoreRouter } = require('@strapi/strapi').factories;
    module.exports = createCoreRouter('api::book.book', {
        config: {
            delete: {
                // register policy to check if user is admin
                policies: ["is-admin"]
            },
        }
    });
```
In the code above, we tell Strapi to configure the book router so as to register and enable the policy we created previously to work on the delete route of our book. Later, we will add some middlewares to the `create` and `update` middlewares of our application.

# Adding Middlewares to Book Routes

At this point, we need to customize our `book`  routes with some middlewares. Middlewares are functions performed before a request gets to a controller.  In Strapi, 2 middleware exists. The one for the entire server application, and the other for routes. In this tutorial, we are using the latter. See [middlewares](https://docs.strapi.io/developer-docs/latest/development/backend-customization/middlewares.html#implementation) for more on Strapi middlewares.  

Prior to hitting the `create` , `update` and `delete` route actions or controllers, we want to be able to know and attach the username of an authenticated user to the request context. This is so that we can easily access it in the controllers. To do this, open the routes file at this location `src/api/book/routes/book.js`.  Replace its content with the following code below:

```
    // path: ./src/api/book/routes/book.js
    
    'use strict';
    /**
     * book router
     */
    const { createCoreRouter } = require('@strapi/strapi').factories;
    module.exports = createCoreRouter('api::book.book', {
        config: {
            create: {
                middlewares: [
                    (ctx, next) => {
                        // check if user is authenticated and save username to context
                        let user = ctx.state.user;
                        if (user) ctx.username = ctx.state.user.username;
                        return next();
                    }
                ]
            },
            update: {
                middlewares: [
                    (ctx, next) => {
                        // check if user is authenticated and save username to context
                        let user = ctx.state.user;
                        if (user) ctx.username = ctx.state.user.username;
                        return next();
                    }
                ]
            },
            delete: {
                // register policy to check if user is admin
                policies: ["is-admin"]
            },
        }
    });
```
In the code above, we configure our book router using the `config` options. 

- **line 10-19**: we specify that authenticated users that want to create a book, we first get their details in **line 14**, then check if the user is actually authenticated in **line 15**. In the same line number, if user is authenticated, add a property to our request context called `username`. This will help us when we customize our controller to be able to add value to the `creator` field of a book.
- **line 20-19**: same as **line 10-19**. This time we specify it for the `update` controller. 


# Creating a Custom like-book Route

Now, aside from adding a review and rating to our book. We want to allow users to like a book. Hence, the need for a custom route. This will be accompanied by a new custom controller. In this folder `src/api/book/routes` , create a file `like-book.js`.  Add the following code:

```
    // path: ./src/api/book/routes/like-book.js
    
    module.exports = {
        routes: [
            {
                method: "PUT",
                path: "/books/:id/like",
                handler: "book.likeBook",
                config: {
                    middlewares: [
                        (ctx, next) => {
                            // check if user is authenticated and save username to context
                            let user = ctx.state.user;
                            if (user) ctx.username = ctx.state.user.username
                            return next();
                        }
                    ]
                }
            }
        ]
    }
```

- **line 6**: we specify that this route accepts a `PUT` request.
- **line 7**: we indicate the path to reach this route. An example is http://127.0.0.1:1337/api/books/3/like. This will send a request to like a book with the id of 3.
- line 8: we specify the handler for this route. As we would see, this will be a custom handler in our book controller.
- **line 9-18**: we added a configuration with a middleware which checks if a user is authenticated and attaches their username to the request context, just as we did for the `update` and `create` of book routes.


# Creating a Custom Controller

It is time to create handlers for our book controller. Open the file located in `src/api/book/controllers/book.js` and replace the code with the following:


https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/43d5052455922f09ce21bd852d12db44


[https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/43d5052455922f09ce21bd852d12db44](https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/43d5052455922f09ce21bd852d12db44)


- **line 11 -51**: `likeBook` handler was created for our like-book custom route. It first checks if the book exits. It then checks if a user has already liked a book through the JSON field `like`  of our book content type by using the `ctx.username` value which we passed as a middleware. If the user already liked the book, it removes the username of the user from the list. Otherwise, it adds it to the list.
- **line 53-62**: we create the `create` handler for requests to create a book. It gets the details of the book we want to create. It also adds another detail to the `creator` field of the book by getting the username of the user sending the request using the `ctx.username` from the middleware we created earlier.
- **line 64-84**: we create the `update` handler for any request to update a book. It gets the `id` request parameter of the book and finds the book using this `id`. It first checks if the book exists. It then checks if the user making the request is actually the creator of the request through the `book.creator` and `ctx.username` values from the book found and the middleware.
- **line 86-101**: the `delete` handler is created to delete a book. It also gets the `id` of the book from the request parameter and finds the book using this `id`. Here we are not checking if the user is the creator of the book. This is because we already created a policy to allow only admin, who is a user with the field `isAdmin` set to `true`, to delete any book.

The full code to the backend server can be found on [GitHub](https://github.com/Theodore-Kelechukwu-Onyejiaku/book-ratings-with-strapijs). 

# Scaffolding The Next.js App
## Creating a NextJs Application

In order to create a NextJs application, we will `cd` into the folder of our choice through the  terminal and run the command below:

```
    npx create-next-app book-ratings
```
The name of our application is `book-ratings`. 

Now run the following command to `cd` into and run our application.

```
    cd book-ratings 
    
    npm run dev
```
If everything works out fine, this is what our browser opens up.


![Default View of a Newly Created NextJs Application](https://paper-attachments.dropboxusercontent.com/s_949B1EB235062E3E50B3B25E987553E5E3DD6BF5079A8375FDF9A9E334519FCD_1626091939285_nextjs.png)


Now, open the folder in any code editor of your choice, and let's start coding the project together.


# Installing Tailwind CSS

We will have to stop our application in order to install Tailwind CSS. We Press `command c` to stop it in our terminal. Next, we install Tailwind CSS. `cd` into the app folder and type the command below:

```
    npm install -D tailwindcss postcss autoprefixer
    npx tailwindcss init -p
```
Next, we open our tailwind.config.js file which is in the root folder of our application, and replace the content with the following code.

```
    // path: ./tailwind.config.js
    
    /** @type {import('tailwindcss').Config} */
    module.exports = {
      content: [
        "./pages/**/*.{js,ts,jsx,tsx}",
        "./components/**/*.{js,ts,jsx,tsx}",
      ],
      theme: {
        extend: {},
      },
      plugins: [],
    }
```
Finally, add this to the global CSS file at `styles/global.css`. 

```
    @tailwind base;
    @tailwind components;
    @tailwind utilities;
```
Now restart our NextJs by running:

```
    npm run dev
```

# Installing Other Dependencies

For our full-fledged application, we also require some npm packages. Run the command below to install them:

```
    npm install axios cloudinary datauri js-cookie multer next-connect react-hot-toast react-icons react-quill
```

- **axios:** this will help us interact with Strapi server API.
- cloudinary: this will allow us to upload book cover images to Cloudinary.
- **multer**: this is a middleware that will help us parse our image from the frontend of our application so that we can 
- **datauri**: this will enable us to convert parsed images to a base 64 encodings. So that we can then upload the converted content to Cloudinary.
- **js-cookie**: this will help us create and retrieve cookies for authentication and authorization.
- **next-connect**: for handling file upload and creating a book record, we will use this in our NextJs handler. It will help us add the Multer middleware.
- **react-hot-toast**: this will be useful for toast notifications.
- **react-icons**: for icons in our application.
- **react-quill**: this will be used for our WYSIWYG editor and for previewing a raw HTML code.


# Building Components in our NextJs App

NextJS is incredible with its component-based architecture, and we can develop our application by splitting the features into minor components. We open our code editor to proceed.


## Layout Component

Firstly, create a new folder in the root directory called `components`. Inside it create a file called  Layout.js. Add the following code. This will help us structure the layout of our application. We would need this late in _app.js of our file. Inside it, we import the `Header` component and `Toaster`  from `react-hot-toast`. It is a high-order function that takes a children function and renders it.

```
    import { Toaster } from 'react-hot-toast';
    import Header from './Header';
    export default function Layout({ children }) {
      return (
        <div>
          <Header />
          <Toaster />
          <div className="content">
            {children}
          </div>
        </div>
      );
    }
    

```
## Header Component

Next, we create the `Header` component of our application. Inside the `components` folder create a file called `Header.js` and add the following code:


https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/0ed5c73c116e8fc63d263d6e2a5091b5


[https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/0ed5c73c116e8fc63d263d6e2a5091b5](https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/0ed5c73c116e8fc63d263d6e2a5091b5)

The code above displays the header of our application. In the code we imported the following:

```
    import Cookies from 'js-cookie';
    import AppContext from '../utils/AppContext';
```
`Cookies` as can be seen in **line 14** of the `logout()`  function removes the cookie `authToken`. This is a token we set when a user logs in to our application. We also imported `AppContext`, a React context API, which includes `authUser` which contains the details of a logged in user. It contains `setAuthUser` which sets the `authUser`. `isLoggedIn` which is a Boolean value to indicate if a user is logged in.  This context is created in the `utils` folder as would be seen soon.

We haven’t yet created the `utils` folder yet and will do so later in the tutorial.


## Books Component

Also, inside the `components` folder create a file `Books.js`. This displays a list of the books users have created. It takes a prop called `books`  which is the list of the books returned by the Strapi server end API. The prop is passed from the `index.js` page as would be seen later.


https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/24a43943d26579c78ee1f972fe136d4a


[https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/24a43943d26579c78ee1f972fe136d4a](https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/24a43943d26579c78ee1f972fe136d4a)

From the code above we imported the following:

```
    import toast from 'react-hot-toast';
    import axios from 'axios';
    import Cookies from 'js-cookie';
    import AppContext from '../utils/AppContext';
    const ReactQuill = dynamic(() => import('react-quill'), { ssr: false });
```

`toast`  will help us display toast notifications. `axios`  will help us make a request to like a book.
We import `Cookies`  in **line 3** so that we get the `authToken` cookie. 

- **line 4**: We also import the `AppContext` so that we can get information about the logged-in user.
- **line 5**: The `ReactQuill` here represents our WYSIWYG editor. Because the `document` object is needed by `ReactQuill` during page render, and is not available we used the `dynamic` provided by NextJs  to disable server-side rendering to fix this issue.

Also from the code above, we have a `handleLikeBook()` function which makes a request to like a book.


## SingleBookAndReview Component

Lastly, inside the components folder, create a file `SingleBookAndReview.js`. This will display a single book when we request it. It will also display the reviews the average ratings of a book, comments, and ratings of each user that reviewed the book. 


https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/58b694bb0381888d47139c3431ac4182


[https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/58b694bb0381888d47139c3431ac4182](https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/58b694bb0381888d47139c3431ac4182)

```
    NEXT_PUBLIC_STRAPI_API=http://127.0.0.1:1337
```
If you have not done so, go ahead and create `.env` file to store your variables.

## Rebuilding the _app.js Page

This page initializes other pages in our application. We will need to update the content of the `_app.js` page. Replace the codes in it with the one below:

```
    import '../styles/globals.css';
    import { useState, useEffect } from 'react';
    import axios from 'axios';
    import Cookies from 'js-cookie';
    import AppContext from '../utils/AppContext';
    import Layout from '../components/Layout';
    function MyApp({ Component, pageProps }) {
      const [authUser, setAuthUser] = useState(null);
      const [isLoggedIn, setIsLoggedIn] = useState(false);
    
      const userAuthentication = async () => {
        try {
          const authToken = Cookies.get('authToken');
          if (authToken) {
            const { data } = await axios.get(`${process.env.NEXT_PUBLIC_STRAPI_API}/api/users/me`, {
              headers: {
                Authorization: `Bearer ${authToken}`,
              },
            });
            setAuthUser(data);
            setIsLoggedIn(true);
          }
        } catch (error) {
          setAuthUser(null);
          setIsLoggedIn(false);
        }
      };
      useEffect(() => {
        userAuthentication();
      }, []);
      return (
        <AppContext.Provider value={{
          authUser, isLoggedIn, setAuthUser, setIsLoggedIn,
        }}
        >
          <Layout>
            <Component {...pageProps} />
          </Layout>
        </AppContext.Provider>
      );
    }
    export default MyApp;
```
From the code above,

- **line 6**: we import the high-order  `Layout`  function component we created previously. 
- **line 11-27**: we create a function `userAuthentication()` which checks if a user is authenticated by. If the user is authenticated, it sets the details of the user in the state variable `authUser` and passes a `true` Boolean value to the `isLoggedIn`  state value.
- **line 28-30**: we run this `userAuthentication()` function any time a user accesses any page using the React  `useEffect` hook.
- **line 32-39**: we wrap the whole codes inside of our React Context API. We then pass `authUser`, `isLoggedIn`, `setAuthUser`, `setIsLoggedIn`, as global state variables using the Context API. Hence, we can access them anywhere in our applicatiion.
- **line 36-38**: we wrap all pages of our application inside the `Layout` component we created earlier on.


## Building the Index.js Page

The `index.js` page will serve as our home page. Replace its content with the following code:

```
    import Head from 'next/head';
    import axios from 'axios';
    import Books from '../components/Books';
    export default function Home({ books, error }) {
      if (error) return <div className="h-screen flex flex-col items-center justify-center text-red-500">Something went wrong!</div>;
      return (
        <div>
          <Head>
            <title>Book Ratings Application</title>
            <meta name="description" content="Generated by create next app" />
            <link rel="icon" href="/favicon.ico" />
          </Head>
          <div>
            <div>
              <Books books={books} />
            </div>
          </div>
        </div>
      );
    }
    export async function getServerSideProps() {
      try {
        const { data } = await axios(`${process.env.NEXT_PUBLIC_STRAPI_API}/api/books`);
        return {
          props: {
            books: data.data,
            error: null,
          },
        };
      } catch (error) {
        return {
          props: {
            books: null,
            error: error.message,
          },
        };
      }
    }
    
```

- **line 3**: we import the `Books` component.
- **line 21-38**: we run a `getServerSideProps()` function which will fetch all books we have created. And it will return this book as a prop to our home page.
- **line 15**: we pass the props `books` returned from the `getServerSideProps()` to the `Book` component. This is so that it will display all our books.

Here is what the home page looks like:


![Home Page of our Application](https://paper-attachments.dropboxusercontent.com/s_159DA53009009462465057F85C8E330FEFD1B79EFE91E697856995F539FCBE6A_1668706297972_Screen+Shot+2022-11-17+at+6.30.38+PM.png)



## Building the Create Page

This is the page that will allow us to create a book for review. 

https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/93f991996acc00d3d44b31accd671020


[https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/93f991996acc00d3d44b31accd671020](https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/93f991996acc00d3d44b31accd671020)


- **line 2**: we import the CSS file for our Quill WYSIWYG editor.
- **line 7**: we import an icon from the `react-icons` package.
- **line 9**: we import `isImage` and `validateSize` functions we created in the `fileValidation.js` of our `utils` folder. The former checks if a file is image. And the latter will check if a file is actually 5 megabytes in size.
- **line 12**: we import the configurations for our WYSIWYG editor. These are found in the `utils` folder of our app which we would create later.
- **line 27-63**: we create the function `createBook()` which sends the request to create a book. Note that it sends this to a NextJs route handler “/api/create-book”. We would create this route handler later in the `API` folder of our application. This handler will handle image upload to cloudinary and creation of a book in NextJs server-side.

Here is what our create book page looks like:


![The Create Book Page View](https://paper-attachments.dropboxusercontent.com/s_159DA53009009462465057F85C8E330FEFD1B79EFE91E697856995F539FCBE6A_1668706383633_Screen+Shot+2022-11-17+at+6.32.32+PM.png)



## Building the edit-book Page

This page allows us to edit a book. This would only allow the creator of the book to edit the book.

```
    import { useState } from 'react';
    import { useRouter } from 'next/router';
    import axios from 'axios';
    import 'react-quill/dist/quill.snow.css';
    import Cookies from 'js-cookie';
    import dynamic from 'next/dynamic';
    import toast from 'react-hot-toast';
    import { author_formats, author_modules } from '../utils/editor';
    const ReactQuill = dynamic(() => import('react-quill'), { ssr: false });
    export default function EditBook({ book, error }) {
      const router = useRouter();
      const successNotification = () => toast.success('Book updated successfully!');
      const errorNotification = (error) => toast.error(error);
      const [info, setInfo] = useState(book?.attributes?.info);
      const [title, setTitle] = useState(book?.attributes?.title);
      const updateBook = async () => {
        const authToken = Cookies.get('authToken');
        try {
          const { data } = await axios.put(
            `${process.env.NEXT_PUBLIC_STRAPI_API}/api/books/${book.id}`,
            { data: { info, title } },
            {
              headers: {
                Authorization: `Bearer ${authToken}`,
              },
            },
          );
          successNotification();
          router.push(`/${book.id}`);
        } catch (error) {
          errorNotification(error.response.data.error.message);
        }
      };
      return (
        <div>
          {error ? <div className="h-screen flex flex-col justify-center items-center text-red-500">{error}</div> : (
            <div className="mx-5">
              <h1 className="text-3xl font-bold">Edit this book</h1>
              <div className="my-5 flex flex-col">
                <label className="font-bold">Edit book title</label>
                <input className="border my-3" value={title} onChange={(e) => { setTitle(e.target.value); }} />
              </div>
              <div className="my-5">
                <label className="font-bold">Edit Book Info</label>
                <ReactQuill className="w-full h-96 pb-10 my-3" onChange={setInfo} formats={author_formats} modules={author_modules} theme="snow" value={info} />
              </div>
              <button type="button" onClick={updateBook} className="shadow p-2 rounded bg-green-500 text-white font-bold">Update Book</button>
            </div>
          )}
        </div>
      );
    }
    export async function getServerSideProps({ query }) {
      const bookId = query.book;
      try {
        const { data: book } = await axios(`${process.env.NEXT_PUBLIC_STRAPI_API}/api/books/${bookId}`);
        return {
          props: { book: book.data, error: null },
        };
      } catch (error) {
        if (error.message === 'connect ECONNREFUSED 127.0.0.1:1337') {
          return {
            props: { book: null, error: 'Connection Error' },
          };
        }
        return {
          props: { book: null, error: error.response.data.error.message },
        };
      }
    }
    
```
The code above basically updates a book.


Our edit book page looks like this:


![The Edit Book Page View](https://paper-attachments.dropboxusercontent.com/s_159DA53009009462465057F85C8E330FEFD1B79EFE91E697856995F539FCBE6A_1668706599941_Screen+Shot+2022-11-17+at+6.36.19+PM.png)



## Building the [id].js Page

This page is responsible for viewing a particular book based on the `id` of the book. So when we visit want to see a book with the `id` of 2, we can just open the URL of our application and add the `id`. For example http://localhost:300/2.


https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/f528d0c98be5bbba73a484f0fd013a02


[https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/f528d0c98be5bbba73a484f0fd013a02](https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/f528d0c98be5bbba73a484f0fd013a02)

For a book to be displayed, we need to fetch the book from our server together with its reviews. Hence in: 

- **line 92-115** we invoke the `getServerSideProps()` function that will pass the book and its reviews and any error using the `id` we passed to the URL. To get the `id` , we query the value from the request or URL using `context.query` value. 
- **line 66:** we pass this book and reviews we got to the `SingleBookAndReview` component we created earlier on. 
- **line 73**: we allow users to add a review comment for this book using our `ReactQuill` WYSIWYG editor. 
- **line 76-83**: also, we allow users to add a rating or score for the book.
- **line 40-62**: we create `addReview()`  function that will make a request to add a review to a book. It takes the comment from the user and the rating score. And sends the authorization token to the server.
- **line 85**: we attach the `addReview()` function to a button.

Here is what a single book and review page looks like:


![A Single Book and Reviews Page View](https://paper-attachments.dropboxusercontent.com/s_159DA53009009462465057F85C8E330FEFD1B79EFE91E697856995F539FCBE6A_1668706858417_Screen+Shot+2022-11-17+at+6.40.37+PM.png)

## Building our login and signup Pages

First, create the signup page by creating a file `signup.js` . 


https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/4b19b515c7b0d087ec607abc34d93659


[https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/4b19b515c7b0d087ec607abc34d93659](https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/4b19b515c7b0d087ec607abc34d93659)


- **line 55**: we make a request for signup.
- **line 61-63**: if signup is successful, we create a cookie `authToken` which is the `jwt` returned from the successful request. Also, we create another cookie `user` that will keep the user details. And we set these cookies to expire in 1 hour.

Our signup page looks like this:


![The Signup Page](https://paper-attachments.dropboxusercontent.com/s_159DA53009009462465057F85C8E330FEFD1B79EFE91E697856995F539FCBE6A_1668706908894_Screen+Shot+2022-11-17+at+6.41.38+PM.png)


Secondly, we create the `login.js` file. 


https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/353828b715a224a51be7ae389c73f0e2


[https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/353828b715a224a51be7ae389c73f0e2](https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/353828b715a224a51be7ae389c73f0e2)

This is basically the same as the `signup.js` page. The only difference is we are making a request to log in.

Here is what our login page looks like:


![View of of Login Page](https://paper-attachments.dropboxusercontent.com/s_159DA53009009462465057F85C8E330FEFD1B79EFE91E697856995F539FCBE6A_1668706944578_Screen+Shot+2022-11-17+at+6.42.14+PM.png)

# Creating create-book Route Handler

This is where we create a handler that will handle requests to create a book. Remember that in creating a book, we as well have to upload the book image to Cloudinary.

To proceed, we need a Cloudinary cloud name, API key, and API secret. Head over to the Cloudinary website to [create an account](https://cloudinary.com?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) and get these three. 

Click on the dashboard tab. There, we will find our API key, API secret, and cloud name. 

![Get the Cloud Name, API Key and API Secret of Our Cloudinary Account](https://paper-attachments.dropboxusercontent.com/s_989AA707FBDB1A6E245B0183CDBE36E7057DC650108E1558D444FCEE5091CDDF_1667441247189_Screen+Shot+2022-11-03+at+3.06.15+AM.png)


Click on **settings**, then click on **upload.** There, you can be able to add presets. 
Give the name and folder the value “**flashcard**” and click ****save**.**

![Create a Preset on Cloudinary](https://paper-attachments.dropboxusercontent.com/s_13D9629A17BA205412E87FA69ADEF80CA77BC359F9970BA6A590DF17482B2D39_1668196350401_Screen+Shot+2022-11-03+at+3.13.08+AM.png)


Add Cloudinary details to `.env.local` file
Create a `.env.local` file and add the following.

```
    // path: ./.env.local
    
    NEXT_PUBLIC_STRAPI_API=http://127.0.0.1:1337
    CLOUDINARY_API_KEY=<our_key>
    CLOUDINARY_API_SECRET=<our_api_secret>
    CLOUDINARY_NAME=<our_cloud_name>

```
Now create the `create-book.js` file inside the `api` of the `pages`  folder. Add the following code:


```
    import nc from 'next-connect';
    import multer from 'multer';
    import DatauriParser from 'datauri/parser';
    import axios from 'axios';
    import path from 'path';
    import cloudinary from '../../utils/cloudinary';
    const handler = nc({
      onError: (err, req, res, next) => {
        res.status(500).end('Something broke!');
      },
      onNoMatch: (req, res) => {
        res.status(404).end('Page is not found');
      },
    })
    // uploading two files
      .use(multer().single('image'))
      .post(async (req, res) => {
        const parser = new DatauriParser();
        const { authToken } = req.cookies;
        const image = req.file;
        try {
          const base64Image = await parser.format(path.extname(image.originalname).toString(), image.buffer);
          const uploadedImgRes = await cloudinary.uploader.upload(base64Image.content, 'ratings', { resource_type: 'image' });
          const imageUrl = uploadedImgRes.url;
          const imageId = uploadedImgRes.public_id;
          const { data } = await axios.post(
            `${process.env.NEXT_PUBLIC_STRAPI_API}/api/books`,
            {
              data: {
                info: req.body.info,
                title: req.body.title,
                imageUrl,
                imageId,
              },
            },
            {
              headers: {
                Authorization: `Bearer ${authToken}`,
              },
            },
          );
          res.json(data);
        } catch (error) {
          res.status(500).json({ error });
        }
      });
    // disable body parser
    export const config = {
      api: {
        bodyParser: false,
      },
    };
    export default handler;
```
From the code above:

- **line 1-6**: we import `next-connect` , so that we can add Multer middleware to our handler. `multer` will be used to parse our form data. `datauri/parser` will be used to convert parsed image file to a base 64 encoding. `path` for getting the extension name of the parsed image file. And finally, our Cloudinary configuration file from the `utils` folder.
- **line 16:** we passed the Multer middleware to parse any image with the form data name `image`  using the `single()` method. Note this was the name on the `create.js` page. 
- **line 23**: we invoke Cloudinary to upload our image using the `uploader.upload()` function. We passed it the image, and the upload preset “ratings” and specified that we want to upload an image using the `resource_type: 'image'`.
- **line 48-52**: we tell NextJs to disable its body parser that we have a parser already which is Multer.


# Creating Utilities for Our Application

So far we have imported some utilities in our components, pages, and API route handler. Let us create these utilities.  First, we create folder `utils`  at the root of our application


## Context API Utility

This provides us with the ability to access states globally. Create a file `AppContext.js` and add the following code:

```
    import { createContext } from 'react';
    const AppContext = createContext({});
    export default AppContext;
```
## Cloudinary Utility

Create a file `cloudinary.js` inside of the `utils` folder and add the following code:

```
    import cloudinary from 'cloudinary';
    cloudinary.v2.config({
      cloud_name: process.env.CLOUDINARY_NAME,
      api_key: process.env.CLOUDINARY_API_KEY,
      api_secret: process.env.CLOUDINARY_API_SECRET,
    });
    export default cloudinary;
    
```
The code above configures our Cloudinary with our Cloudinary cloud name, API key, and API secret. Remember that this file was imported inside of the `create-book` request handler.


## Editor Utility

This configures the **WYSIWYG** Quill editor in our pages and components.  Create a file `editor.js` and add the following code:

```
    export const author_modules = {
      toolbar: [
        [{ header: [1, 2, false] }],
        ['bold', 'italic', 'underline', 'strike', 'blockquote'],
        [{ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' }],
        ['link'],
        ['clean'],
      ],
    };
    export const author_formats = [
      'header',
      'bold', 'italic', 'underline', 'strike', 'blockquote',
      'list', 'bullet', 'indent',
      'link',
    ];
    export const reviewer_modules = {
      toolbar: [
        [{ header: [1, 2, false] }],
        ['bold', 'italic', 'underline', 'strike', 'blockquote'],
        [{ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' }],
        ['link', 'image'],
        ['clean'],
      ],
    };
    export const reviewer_formats = [
      'header',
      'bold', 'italic', 'underline', 'strike', 'blockquote',
      'list', 'bullet', 'indent',
      'link', 'image',
    ];
    
```

- **line 1 and 10**: we define the editor for an author. This is the editor configuration for a user that wants to create a Book. 
- **line 16 and 25**: we specify the editor for a reviewer. This is for a user to add a review for a book. 


> **NOTE**: the only difference is that author’s editor has `image`  support removed. This is because we are using Cloudinary.


## File Validation Utility

This file will be responsible for validating any image we want to upload. Create a file called `fileValidation.js` . Then add the following code:

```
    export const validateSize = (file) => {
      if (!file) return;
      // if greater than 5MB
      if (file.size > 5000000) {
        return true;
      }
      return false;
    };
    const getExtension = (filename) => {
      const parts = filename.split('.');
      return parts[parts.length - 1];
    };
    export const isImage = (filename) => {
      const ext = getExtension(filename);
      switch (ext.toLowerCase()) {
        case 'jpg':
        case 'gif':
        case 'bmp':
        case 'png':
        case 'jpeg':
          return true;
        default:
          return false;
      }
    };
    

```

- line 1: the `validateSize()`  method will check if the image is less or equal to 5 megabytes and returns a corresponding Boolean result `true` or `false`.
- line 9: the `getExtension` helps us get the extension of a file that we want to upload. It is called in the `isImage()` method in **line 13.**
- line 13: we invoke the `isImage()` function to check if the file we want to upload is an image. 


> NOTE: these methods are invoked in the pages of our application. A corresponding toast notification is displayed if the conditions are not met.



# Testing Finished App

Our application looks like the one here:


https://www.youtube.com/watch?v=cLS8g1D0rSc&


[https://youtu.be/cLS8g1D0rSc](https://youtu.be/cLS8g1D0rSc)

# Conclusion

In this tutorial, we have looked at how to create a Book Ratings application. We added policy, middleware, custom controllers, and routes to Strapi Server API. We were able to upload images to Cloudinary and utilize Strapi Ratings Plugin. 

Strapi is great in so many ways. You only have to use it. Here is the full code to our application https://github.com/Theodore-Kelechukwu-Onyejiaku/book-ratings-with-strapijs.