DEV Community

Strapi
Strapi

Posted on • Originally published at strapi.io

Create a Book Rating App with Strapi Ratings Plugin and Cloudinary

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 tutorial for installation guide.
  2. Basic understanding of Strapi - get started with this quick guide.
  3. Basic knowledge of Next.js
  4. Basic knowledge of Tailwind CSS

Github URL

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

After installation, you should see a new tab for ratings plugin.

Strapi Ratings Plugin Initial View

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

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

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

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.

Ensure that this new field has a default value false.

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

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

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

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

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;
    };
Enter fullscreen mode Exit fullscreen mode
  • 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"]
            },
        }
    });
Enter fullscreen mode Exit fullscreen mode

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 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"]
            },
        }
    });
Enter fullscreen mode Exit fullscreen mode

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();
                        }
                    ]
                }
            }
        ]
    }
Enter fullscreen mode Exit fullscreen mode
  • 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

  • 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.

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

If everything works out fine, this is what our browser opens up.

Default View of a Newly Created NextJs Application

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
Enter fullscreen mode Exit fullscreen mode

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: [],
    }
Enter fullscreen mode Exit fullscreen mode

Finally, add this to the global CSS file at styles/global.css.

    @tailwind base;
    @tailwind components;
    @tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Now restart our NextJs by running:

    npm run dev
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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>
      );
    }


Enter fullscreen mode Exit fullscreen mode

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

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';
Enter fullscreen mode Exit fullscreen mode

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

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 });
Enter fullscreen mode Exit fullscreen mode

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

    NEXT_PUBLIC_STRAPI_API=http://127.0.0.1:1337
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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,
          },
        };
      }
    }

Enter fullscreen mode Exit fullscreen mode
  • 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

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

  • 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

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 },
        };
      }
    }

Enter fullscreen mode Exit fullscreen mode

The code above basically updates a book.

Our edit book page looks like this:

The Edit Book Page View

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

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

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

  • 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

Secondly, we create the login.js file.

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

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 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

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

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>

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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',
    ];

Enter fullscreen mode Exit fullscreen mode
  • 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;
      }
    };


Enter fullscreen mode Exit fullscreen mode
  • 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

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.

Top comments (0)