Learn how to create a book rating application. This tutorial will introduce you to Cloudinary, Next.js, implementing a WYSIWYG editor, Strapi custom controllers, middleware, and policies.
Simply copy and paste the following command line in your terminal to create your first Strapi project.
npx create-strapi-app
my-project
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.
Before we start, we need to equip ourselves with the following:
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.
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.
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 new Strapi project is very simple and works precisely as installing a new frontend framework.
We will start by running the following commands and testing them out in our default browser.
1
2
3
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.
This plugin will allow us to add reviews to our application through some API routes. Run the command below to install:
1
npm install strapi-plugin-ratings
After installation, you should see a new tab for rating plugin.
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 |
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
.
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.
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:
1
2
3
4
5
6
7
8
9
10
11
// 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;
};
isAdmin
is false
for every user. However, an admin will have a true
value for the isAdmin
field. 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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 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.
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 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.
username
. This will help us when we customize our controller to be able to add value to the creator
field of a book.update
controller. 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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();
}
]
}
}
]
}
PUT
request.update
and create
of book routes.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
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.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.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.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.
To create a NextJs application, we will cd
into the folder of our choice through the terminal and run the command below:
1
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.
1
2
3
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.
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:
1
2
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
// 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
.
1
2
3
@tailwind base;
@tailwind components;
@tailwind utilities;
Now restart our NextJs by running:
1
npm run dev
For our full-fledged application, we also require some npm packages. Run the command below to install them:
1
npm install axios cloudinary datauri js-cookie multer next-connect react-hot-toast react-icons react-quill
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.
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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>
);
}
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:
1
2
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.
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:
1
2
3
4
5
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.
AppContext
so that we can get information about the logged-in user.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.
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
1
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.
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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,
Layout
function component we created previously. 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.userAuthentication()
function any time a user accesses any page using the React useEffect
hook.authUser
, isLoggedIn
, setAuthUser
, setIsLoggedIn
, as global state variables using the Context API. Hence, we can access them anywhere in our applicatiion.Layout
component we created earlier on.The index.js
page will serve as our home page. Replace its content with the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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,
},
};
}
}
Books
component.getServerSideProps()
function which will fetch all books we have created. And it will return this book as a prop to our home page.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:
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
react-icons
package.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.utils
folder of our app which we would create later.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:
This page allows us to edit a book. This would only allow the creator of the book to edit the book.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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:
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:
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. SingleBookAndReview
component we created earlier on. ReactQuill
WYSIWYG editor. 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.addReview()
function to a button.Here is what a single book and review page looks like:
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
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 sign-up 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:
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.
1
2
3
4
5
6
// 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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:
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.image
using the single()
method. Note this was the name on the create.js
page. 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'
.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
This provides us with the ability to access states globally. Create a file AppContext.js
and add the following code:
1
2
3
import { createContext } from 'react';
const AppContext = createContext({});
export default AppContext;
Create a file cloudinary.js
inside of the utils
folder and add the following code:
1
2
3
4
5
6
7
8
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.
This configures the WYSIWYG Quill editor in our pages and components. Create a file editor.js
and add the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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',
];
NOTE: the only difference is that author’s editor has
image
support removed. This is because we are using Cloudinary.
This file will be responsible for validating any image we want to upload. Create a file called fileValidation.js
. Then add the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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;
}
};
validateSize()
method will check if the image is less or equal to 5 megabytes and returns a corresponding Boolean result true
or false
.getExtension
helps us get the extension of a file that we want to upload. It is called in the isImage()
method in line 13.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.
Our application looks like the one here:
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.
Theodore is a Technical Writer and a full-stack software developer. He loves writing technical articles, building solutions, and sharing his expertise.