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:
- NodeJs installed on our local machine. See tutorial for installation guide.
- Basic understanding of Strapi - get started with this quick guide.
- Basic knowledge of Next.js
- Basic knowledge of Tailwind CSS
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.
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
.
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
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.
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.
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.
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.
Next, we allow access to authenticated users to create a review, find a review, and get users’ reviews for a particular book.
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
isfalse
for every user. However, an admin will have atrue
value for theisAdmin
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 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 thecreator
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
andcreate
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 fieldlike
of our book content type by using thectx.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 thecreator
field of the book by getting the username of the user sending the request using thectx.username
from the middleware we created earlier. -
line 64-84: we create the
update
handler for any request to update a book. It gets theid
request parameter of the book and finds the book using thisid
. It first checks if the book exists. It then checks if the user making the request is actually the creator of the request through thebook.creator
andctx.username
values from the book found and the middleware. -
line 86-101: the
delete
handler is created to delete a book. It also gets theid
of the book from the request parameter and finds the book using thisid
. 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 fieldisAdmin
set totrue
, 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
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.
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
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
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 thedocument
object is needed byReactQuill
during page render, and is not available we used thedynamic
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
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 variableauthUser
and passes atrue
Boolean value to theisLoggedIn
state value. -
line 28-30: we run this
userAuthentication()
function any time a user accesses any page using the ReactuseEffect
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 thegetServerSideProps()
to theBook
component. This is so that it will display all our books.
Here is what the home page looks like:
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
andvalidateSize
functions we created in thefileValidation.js
of ourutils
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 theAPI
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:
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:
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 theid
we passed to the URL. To get theid
, we query the value from the request or URL usingcontext.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:
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 thejwt
returned from the successful request. Also, we create another cookieuser
that will keep the user details. And we set these cookies to expire in 1 hour.
Our signup page looks like this:
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:
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.
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.*
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 theutils
folder. -
line 16: we passed the Multer middleware to parse any image with the form data name
image
using thesingle()
method. Note this was the name on thecreate.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 theresource_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 resulttrue
orfalse
. - line 9: the
getExtension
helps us get the extension of a file that we want to upload. It is called in theisImage()
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&
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)