- Published on
How to add a voting widget to Next.js project
In this tutorial we learn how to add a widget with article rating to the next.js static website.
The thing is that it is a challenge to find a solution to this issue: all lessons are aimed at making a beautiful static widget, but there is no possibility to save the values, for example, in a database. There are also ready-made plugin widgets that can be used, but I don't see the point in paying for this feature.
How the widget is organized
First I will briefly describe how this widget will work.
The website (I use starter tailwind-nextjs-starter-blog) has published articles on it, and each article has its own rating.
Features:
- The visitor can click on the upvote or downvote button.
- Rating (karma) is calculated according to the formula: like minus dislike. The result number can be positive or negative.
- It is also necessary that the user cannot vote twice - the voting button must be deactivated by css.
- Voting does not require authorization on the website.
- The data is stored in a database, each page has its own rating, which is linked to the page url.
- If there is no information about the page in the database, the widget will not be displayed on the website.
Adding pages to the table
First, we need to import page urls information into the database. There will be 3 columns in total:
- Relative URL (text) - this data can be grabbed from the sitemap
- like column (int8) with value 0
- dislike column (int8) with value 0
You can import csv data to Supabase easily using the Import button.
Each row, by default, is assigned an id and a creation date. This is not important in this case, the string binding will be done based on page.
I will skip the explanation on how to add supabase to the next js project, all information is available on the official website. Also, you should use RLS to create Policies for read and update operations. In the case of the public tables it is obligatory to do this.
Create a component with voting buttons
The user will click on the upvote or downvote button. The result will be displayed next to the buttons as a number - the difference between likes and dislikes. It can be either a positive or negative value.
I use TailwindCSS for styling (besides it's already integrated into my next.js starter), but for simple tasks like this you can get by without it. Here's a link to setting up Tailwind in the next js project.
<div className='flex bg-gray-50 justify-center justify-items-center items-center p-4 my-20 border-solid border border-gray-200 rounded-md'>
<div className="inline-block align-middle">
<p>Was the information helpful? </p>
</div>
<div className="ml-10 flex gap-3">
<button className='bg-white hover:bg-gray-100 py-2 px-4 border border-gray-400 rounded shadow no-underline'>👍 Yes
</button>
<button className='bg-white hover:bg-gray-100 py-2 px-4 border border-gray-400 rounded shadow no-underline'👎 No
</button>
</div>
<p className="mx-5">Karma:</p>
<button className='bg-white hover:bg-gray-100 py-2 px-4 border border-gray-400 rounded shadow no-underline'
><span className="m-2 bg-blue-50 text-blue-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded-full dark:bg-blue-900 dark:text-blue-300 ">0</span>
</button>
</div>
Next, let's add the following state variables to our Like component: like, dislike, disableLike and counter.
const [counter, setCounter] = useState(0);
const [like, setLike] = useState()
const [dislike, setDislike] = useState()
const [disableLike, setDisableLike] = useState(false)
disableLike is used to disable the button after voting, so that the user cannot click on it. Counter is the total value which shows whether the article is helpful or not.
Adding Supabase client
Next we will connect Supabase to the voting component:
import supabase from "@/lib/supabase"
The supabase.js file looks like this:
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
const supabase = createClient(supabaseUrl, supabaseKey);
export default supabase;
Add data to the .env.local file (credentials data can be found under Settings - API in Supabase Dashboard:
NEXT_PUBLIC_SUPABASE_URL="https://***"
NEXT_PUBLIC_SUPABASE_ANON_KEY="***"
After updating .env.local, restart the server.
Adding useEffect and useRouter hooks
We use the following function to read the Like and Dislike values via useEffect.
useEffect(function () {
async function getItems() {
const { data: likes, error } = await supabase
.from("likes")
.select()
.eq("page", router.asPath)
.single();
setLike(likes.like)
setDislike(likes.dislike)
setCounter(likes.like - likes.dislike)
}
getItems();
}, [incrementLike, incrementDislike]);
From the Likes table we read the row associated with the current page. We pass the data to like and dislike buttons. Then we update Counter.
incrementLike / incrementDislike are dependencies, after updating them we trigger useEffect again. Each time we update the rating, we will see the changes in Counter.
Note. Pay attention to the destructuring { data: likes, error }
. It is necessary to take into account the component behavior in case of missing data in the database. Also, it is necessary to add a spinner or a message to the page informing users about the loading process (for example, create a <Loading />
component and specify state isLoading true and false).
The useRouter hook should be imported into the project.
import { useRouter } from 'next/router'
We use router.asPath to get the current URL of the page to get the number of likes and dislikes when querying the database.
Also, we involve the incrementLike and incrementDislike functions to update likes and dislikes values. We then set setDisableLike to true to deactivate the voting buttons.
The code to update the Dislike value in the database should look like.
async function incrementLike () {
const { data: likes, error } = await supabase
.from("likes")
.update({ like: like + 1 })
.eq("page", router.asPath)
setDisableLike(true)
};
Similar code is used to update the Dislike value.
async function incrementDislike () {
const { data: likes, error } = await supabase
.from("likes")
.update({ dislike: dislike + 1 })
.eq("page", router.asPath)
setDisableLike(true)
};
So the data will be written to the Likes table. Then we set the value setDisableLike(true), which is initially false.
Let's go back to the button code. We update the Like value when the buttons are clicked.
<button className='bg-white hover:bg-gray-100 py-2 px-4 border border-gray-400 rounded shadow no-underline' disabled={disableLike} onClick={()=>incrementLike()}>👍 Yes
</button>
The same for Dislike.
<button className='bg-white hover:bg-gray-100 py-2 px-4 border border-gray-400 rounded shadow no-underline' disabled={disableLike}
onClick={()=>incrementDislike()}>👎 No
</button>
If necessary, you can make separate counters for likes and dislikes. For example:
<span className="m-2 bg-blue-50 text-blue-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded-full dark:bg-blue-900 dark:text-blue-300 ">{like}</span>
Accordingly, for dislike we use the dislike value in the span element. After that indicators will be displayed on the buttons.
Conditional formatting
You can make conditional formatting for the counter. Red color - negative number, green - the positive one. Zero is associated with no color.
I wouldn't split the code of the button into a separate component as I apply the color only for the final value:
<span className={`m-2 mr-2 rounded-full px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-300 ${counter >= 0 ? 'bg-blue-50' : 'bg-red-50'}`}>
{counter}
</span>
After clicking the Yes or No button, the corresponding value is written to the table. The counter data is updated. The user will no longer be able to vote - until the page is reloaded.
What can be improved
Of course, this is not the final version of the voting widget, and there are things to think about, for example:
- You could write the data to LocalStorage and prevent the user from voting after the page reloads.
- In case of an error (no page in the database) do not display the widget on the page.
I will post the widget code with its further modifications on github.