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 and uuid)

Step 1: Set Up Environment Variables

Create a .env.local file in the root of your project and add your Vercel PostgreSQL database credentials:

bash
POSTGRES_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:

mdx
import 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.