Like Button
Published on: 3/1/2024
In this article, we will walk you through the process of creating a like button for an MDX-based Next.js application. Our goal is to support both authenticated and anonymous users by leveraging local storage and a PostgreSQL database hosted on Vercel. This ensures a seamless user experience while maintaining data persistence.
Prerequisites
Before we start, ensure you have the following:
- A Next.js project setup
- Vercel PostgreSQL database credentials
- Necessary npm packages installed (
react-icons
anduuid
)
Step 1: Set Up Environment Variables
Create a .env.local
file in the root of your project and add your Vercel PostgreSQL database credentials:
bashPOSTGRES_URL="************"
POSTGRES_PRISMA_URL="************"
POSTGRES_URL_NO_SSL="************"
POSTGRES_URL_NON_POOLING="************"
POSTGRES_USER="************"
POSTGRES_HOST="************"
POSTGRES_PASSWORD="************"
POSTGRES_DATABASE="************"
Step 2: Create the LikeButton Component
Create a new file components/like-button.tsx
and add the following code:
typescript"use client";
import React, { useState, useEffect } from "react";
import { v4 as uuidv4 } from "uuid";
import { AiOutlineLike, AiFillLike } from "react-icons/ai";
import {
countLikes,
addLike,
removeLike,
isPostLikedByUser, // New function for checking if the post is liked
} from "@/app/actions/like-actions";
interface LikeButtonProps {
postId: number;
}
function LikeButton({ postId }: LikeButtonProps) {
const [liked, setLiked] = useState(false);
const [userId, setUserId] = useState<string | null>(null);
const [totalLikes, setTotalLikes] = useState<number>(0);
const [loading, setLoading] = useState(true);
const [likeActionInProgress, setLikeActionInProgress] = useState(false);
useEffect(() => {
const fetchUserId = () => {
let storedUserId = localStorage.getItem("userIdForMDXBlog");
if (!storedUserId) {
storedUserId = uuidv4(); // For testing purposes, otherwise get it from your auth system
localStorage.setItem("userIdForMDXBlog", storedUserId);
}
setUserId(storedUserId);
};
fetchUserId();
fetchTotalLikes();
}, [postId]);
useEffect(() => {
if (userId) {
checkIfLikedByUser();
}
}, [userId]);
async function fetchTotalLikes() {
try {
const response = await countLikes(postId);
if (response.success && typeof response.count === "number") {
setTotalLikes(response.count);
} else {
setTotalLikes(0); // Default to 0 if there's an error
}
} catch (error) {
console.error("Failed to fetch likes:", error);
setTotalLikes(0); // Default to 0 in case of failure
} finally {
setLoading(false); // Data has been loaded
}
}
async function checkIfLikedByUser() {
try {
const response = await isPostLikedByUser(postId, userId as string);
if (response.success) {
setLiked(response.liked); // Set liked status based on the database
}
} catch (error) {
console.error("Failed to check if post is liked by user:", error);
}
}
async function handleLikeAction() {
if (userId && !likeActionInProgress) {
setLikeActionInProgress(true);
const response = await addLike(postId, userId);
if (response.success) {
setLiked(true);
setTotalLikes((prev) => prev + 1);
}
setLikeActionInProgress(false);
}
}
async function handleUnlikeAction() {
if (userId && !likeActionInProgress) {
setLikeActionInProgress(true);
const response = await removeLike(postId, userId);
if (response.success) {
setLiked(false);
setTotalLikes((prev) => prev - 1);
}
setLikeActionInProgress(false);
}
}
if (loading) {
return null; // Hide component while loading
}
return (
<div className="py-8">
<div>Total Likes: {totalLikes}</div>
<button
type="button"
onClick={() => {
liked ? handleUnlikeAction() : handleLikeAction();
}}
className={`like-button bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded flex items-center ${
liked ? "liked" : "unliked"
}`}
disabled={likeActionInProgress}
>
{liked ? (
<AiFillLike className="text-xl" />
) : (
<AiOutlineLike className="text-xl" />
)}
{liked ? "Liked" : "Like"}
</button>
</div>
);
}
export default LikeButton;
Step 3: Create Server Actions for Database Interaction
Create a new file app/actions/like-actions.ts
and add the following code:
typescript"use server";
import { sql } from "@vercel/postgres";
export async function countLikes(postId: number) {
try {
const result = await sql`
SELECT COUNT(*) as count FROM likes_for_test
WHERE postid = ${postId};
`;
if (result.rows && result.rows.length > 0) {
return {
success: true,
count: Number.parseInt(result.rows[0].count, 10),
};
} else {
return { success: false, error: "No results found" };
}
} catch (error) {
return { success: false, error: error.message };
}
}
export async function addLike(postId: number, userId: string) {
try {
const result = await sql`
INSERT INTO likes_for_test (postid, userid)
VALUES (${postId}, ${userId});
`;
return { success: true, result };
} catch (error) {
return { success: false, error: error.message };
}
}
export async function removeLike(postId: number, userId: string) {
try {
const result = await sql`
DELETE FROM likes_for_test
WHERE postid = ${postId} AND userid = ${userId};
`;
return { success: true, result };
} catch (error) {
return { success: false, error: error.message };
}
}
export async function isPostLikedByUser(postId: number, userId: string) {
try {
const result = await sql`
SELECT 1 FROM likes_for_test
WHERE postid = ${postId} AND userid = ${userId}
LIMIT 1;
`;
return { success: true, liked: result.rows.length > 0 };
} catch (error) {
return { success: false, error: error.message, liked: false };
}
}
Step 4: Embed the LikeButton in Your MDX File
To embed the LikeButton
component in your MDX content, use the following code:
mdximport LikeButton from "../components/like-button"; export const metadata = { title: "Like Button Article", publishDate: "2024-03-01T00:00:00Z", id: 1, }; Content <LikeButton postId={metadata.id} />
Conclusion
By following these steps, you have successfully implemented a like button system that supports both anonymous and authenticated users in your Next.js MDX app. The component leverages local storage for user identification and integrates a PostgreSQL database hosted on Vercel for persisting like data, ensuring a seamless experience across sessions and devices.