DEV Community

Cover image for How to Add a Comment Section to your ReactJS App (Vite/NextJS)
Tsabary
Tsabary

Posted on

How to Add a Comment Section to your ReactJS App (Vite/NextJS)

Fostering meaningful conversations within your app can make or break user engagement. Whether it's a blog, a social media platform, or an e-commerce app, providing users with a space to share their thoughts and connect with others elevates the experience to a whole new level. That's where a modern, fully-featured comment section comes in.

In this article, I'll show you exactly how to integrate a comment section into your ReactJS app. For this guide I will be using Vite, but exactly the same code could be used in a NextJS or other ReactJS based framework

If you are a mobile developer, I also wrote a similar guide for implementation in a React Native app. You can check it out over here.

By the end of this guide, you'll have a dynamic comment section up and running in no time.

For the comment section itself, we’ll be using Replyke, a versatile tool that brings a host of exciting features to the table:

  • Mentions to tag users directly.
  • Threaded replies to keep conversations structured.
  • Likes for instant feedback.
  • Highlighted comments and replies for emphasis.
  • GIF support to add personality.
  • Built-in authorization to ensure only authenticated users can interact.
  • User reporting to flag inappropriate content.
  • A powerful back office for moderation, allowing you to easily manage flagged content and take action when needed.

These features make Replyke more than just a comment section—it’s a tool for building community and trust within your app.

To keep things simple and focused, we’ll use static dummy data for our entities (posts or articles) and a logged-in user. In a production environment, you’d connect this to your dynamic content and user management system, but the implementation steps remain identical.

Why is adding a comment section so important? Beyond giving users a voice, it opens the door to richer interactions, improved retention, and higher app engagement. Plus, with Replyke’s moderation features baked in, you can ensure your app remains a safe and welcoming space for everyone. Moderation is often an afterthought, but it’s essential for managing user-generated content, and having a back office built right in is a game-changer.

This article is a step aside from my ongoing series, "How to Build a Social Network in 1 Day." If you’re as excited as I am about the power of Replyke, I encourage you to check out that series too. But for now, let’s dive in and create an engaging, fully-functional comment section for your app!

Part 1 is for basic setup for those who start from scratch for the sake of this tutorial. If you already have an existing app you can skip this step. That said, it is recommended to follow this guide as a separate app for a smooth experience first, and simply copy any code to your own app when done.

At the end of every major milestone you would find a link to GitHub with the source code up to that point.


Part 1: Basic repository setup

Setting Up the Project

To get started, navigate to the directory where you want to create your repository. Open your terminal and run the following command:

npm create vite@latest comments-tutorial -- --template react-ts
Enter fullscreen mode Exit fullscreen mode

This initializes a new React project with TypeScript using Vite in your chosen directory.

Next, navigate into the newly created repository and install the necessary dependencies:

cd comments-tutorial
npm install
Enter fullscreen mode Exit fullscreen mode

Cleaning Up the Project Structure

Before proceeding, let’s clean up unnecessary files:

  1. Delete the following files:

    • Any files inside the public/ directory
    • Any files inside src/assets/
    • src/App.css
  2. Clear the contents of these files (but keep them):

    • src/index.css
    • src/App.tsx

After clearing src/App.tsx, it should look like this:

function App() {
  return <div>Hello World</div>;
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Installing Tailwind CSS

For easy styling, install Tailwind CSS by running:

npm install tailwindcss @tailwindcss/vite
Enter fullscreen mode Exit fullscreen mode

Next, update your Vite configuration to include Tailwind CSS. Open vite.config.ts and modify it to look like this:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [react(), tailwindcss()]
});
Enter fullscreen mode Exit fullscreen mode

Then, add the following import statement to your empty src/index.css file:

@import "tailwindcss";
Enter fullscreen mode Exit fullscreen mode

Now, verify Tailwind is working by updating src/App.tsx:

function App() {
  return <div className="min-h-screen bg-red-500">Hello World</div>;
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Run the development server:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Open your browser, and you should see a red screen with "Hello World" in the top-left corner. If not, double-check the steps above or refer to the official Tailwind CSS documentation for any recent changes.


Setting Up ShadCN

Before proceeding with Replyke, let's configure ShadCN for UI components.

Modify tsconfig.json by adding the following inside compilerOptions at the root level (as a sibling of references):

  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
Enter fullscreen mode Exit fullscreen mode

Modify tsconfig.app.json by adding the following under compilerOptions:

    "baseUrl": ".",
    "paths": {
      "@/*": [
        "./src/*"
      ]
    }
Enter fullscreen mode Exit fullscreen mode

Install Node types as a dev dependency:

npm install -D @types/node
Enter fullscreen mode Exit fullscreen mode

Update vite.config.ts to resolve alias paths. Your final file should look like this:

import path from "path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [react(), tailwindcss()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Initialize ShadCN:

npx shadcn@latest init
Enter fullscreen mode Exit fullscreen mode

You can simply use all the default values during the setup.

Lastly, add the components we will need by running

npx shadcn@latest add button drawer scroll-area sheet sonner
Enter fullscreen mode Exit fullscreen mode

That’s it for the setup! You can check the repository state at this point.
Part 1 completed source code on GitHub for reference.

Part 2: Setting Up Replyke in Your Project

With our basic app setup complete and running, it's time to integrate Replyke and bring our comment section to life. This involves setting up a new Replyke project and adding its provider to our app. Unlike full-scale authentication setups, we'll be using a mock external user system for this tutorial. Replyke expects a signedToken prop containing a signed JWT with user details, and we’ll configure this later. For now, let’s focus on the foundational setup of Replyke itself.

Steps:

  1. Create a Project on Replyke Dashboard: Head over to dashboard.replyke.com, create an account if you don’t already have one, and start a new project. Once the project is created, copy the provided Project ID into a .env file.

  2. Install Replyke for ReactJS: Open your terminal and run the following command to install Replyke for your Expo project:

    npm install @replyke/react-js
    
  3. Wrap Your App with ReplykeProvider: In your src/App.tsx file, wrap your app’s content with the ReplykeProvider component from Replyke. Pass the Project ID you copied earlier as the projectId prop. This initializes Replyke within your app.

Here’s how your src/App.tsx file should look like after making these changes (we're also adding the sonner Toaster while we're at it):

import { ReplykeProvider } from "@replyke/react-js";
import { Toaster } from "@/components/ui/sonner";

function App() {
  return (
    <ReplykeProvider projectId={import.meta.env.VITE_PUBLIC_REPLYKE_PROJECT_ID}>
      <Toaster />
      <div>Hello world</div>
    </ReplykeProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

With these steps completed, Replyke is now set up and ready to power your comment section.

Part 3: Creating Dummy Data

To test our comment section, we’ll need some sample data for posts and users. This will allow us to simulate the interaction between users and the comment system. Let’s create a file to store our dummy data.

  1. Create a Constants Folder: Inside your project’s src directory, create a folder named constants.

  2. Add the Dummy Data File: In the constants folder, create a new file called dummy-data.ts and paste the following code:

    // Dummy data for posts
    const posts = [
      { id: "post1", content: "This is the first post." },
      { id: "post2", content: "Hello world! This is post two." },
      { id: "post3", content: "Another day, another post." },
      { id: "post4", content: "Simple content for post four." },
      { id: "post5", content: "Post five is here." },
      { id: "post6", content: "Sharing thoughts in post six." },
      { id: "post7", content: "This is the seventh post." },
      { id: "post8", content: "Post eight with simple text." },
      { id: "post9", content: "The ninth post is ready." },
      { id: "post10", content: "Finally, this is the tenth post." },
    ];

    // Dummy data for users
    const users = [
      { id: "user1", username: "john123" },
      { id: "user2", username: "emma456" },
      { id: "user3", username: "mike789" },
      { id: "user4", username: "sarah202" },
    ];

    export { posts, users };
Enter fullscreen mode Exit fullscreen mode
  1. Save and Import the Dummy Data: With the dummy data file in place, we can now use these posts and users in our app to simulate interactions.

Render posts

Lastly, let's present our dummy posts in our home screen in a nice grid. Replace the contents of src/App.tsx with the following:

import { ReplykeProvider } from "@replyke/react-js";
import { Toaster } from "@/components/ui/sonner";
import { posts } from "./constants/dummy-data";

function App() {
  return (
    <ReplykeProvider projectId={import.meta.env.VITE_PUBLIC_REPLYKE_PROJECT_ID}>
      <Toaster />
      <div className="h-screen grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 p-6 sm:p-8 md:p-12 lg:p-24 xl:p-52 gap-4 md:gap-8 lg:gap-12 m-auto">
        {posts.map((post) => (
          <div className="bg-white p-4 mb-4 rounded-xl shadow-lg">
            {/* Post Content */}
            <p className="text-lg font-bold text-gray-800">{post.id}</p>
            <p className="text-gray-600 mt-2">{post.content}</p>

            {/* Open Discussion Button */}
            <Button className="w-full mt-4 cursor-pointer">
              Open Discussion
            </Button>
          </div>
        ))}
      </div>
    </ReplykeProvider>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

You should now see all of our dummy posts when you open the app.

Parts 2 and 3 completed source code on GitHub for reference.

Part 4: Adding the Comment Section

With our app setup complete and displaying posts, it’s time to integrate the Replyke comment section. This will allow users to engage in discussions on each post.

Installing the Social Comment Package

To begin, install the Replyke social comments package:

npm install @replyke/comments-social-react-js
Enter fullscreen mode Exit fullscreen mode

Ensuring Responsiveness

We want our app to be fully responsive. To achieve this, we’ll use both the Drawer and Sheet components from ShadCN, displaying one or the other depending on the screen size.

To implement this, we first need a utility hook to detect screen size.

Creating a Media Query Hook

Inside the src folder, create a new directory called hooks, and within it, create a file named useMediaQuery.tsx. Add the following code:

import { useState, useEffect } from "react";

export function useMediaQuery(query: string): boolean {
  const [matches, setMatches] = useState(false);

  useEffect(() => {
    const media = window.matchMedia(query);
    if (media.matches !== matches) {
      setMatches(media.matches);
    }
    const listener = () => setMatches(media.matches);
    window.addEventListener("resize", listener);
    return () => window.removeEventListener("resize", listener);
  }, [matches, query]);

  return matches;
}
Enter fullscreen mode Exit fullscreen mode

This hook allows us to detect whether the screen size meets a specified media query.

Creating the DiscussionSheet Component

Now, let’s create the comment section component. Inside the src/components directory, create a file named DiscussionSheet.tsx and add the following:

import { useEntity } from "@replyke/react-js";
import {
  SocialStyleCallbacks,
  useSocialComments,
  useSocialStyle,
} from "@replyke/comments-social-react-js";
import { toast } from "sonner";
import { Sheet, SheetContent } from "@/components/ui/sheet";
import { Drawer, DrawerContent } from "@/components/ui/drawer";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useMediaQuery } from "@/hooks/useMediaQuery";

export function DiscussionSheet({
  isSheetOpen,
  onClose,
}: {
  isSheetOpen: boolean;
  onClose: () => void;
}) {
  const isDesktop = useMediaQuery("(min-width: 768px)");
  const { entity } = useEntity();

  const callbacks: SocialStyleCallbacks = {
    loginRequiredCallback: () => {
      toast("Please log in first");
    },
    // More callbacks can be used to further customize the comment section’s behavior.
  };

  const styleConfig = useSocialStyle(); // This is required, even if using default styles.

  const { CommentSectionProvider, SortByButton, CommentsFeed, NewCommentForm } =
    useSocialComments({
      entityId: entity?.id,
      styleConfig,
      callbacks,
    });

  const sortByOptions = (
    <div className="flex px-6 items-center gap-1">
      <h4 className="font-semibold text-base flex-1">Comments</h4>
      <SortByButton priority="top" activeView={<div className="bg-black py-1 px-2 rounded-md text-white text-xs">Top</div>} nonActiveView={<div className="bg-gray-200 py-1 px-2 rounded-md text-xs">Top</div>} />
      <SortByButton priority="new" activeView={<div className="bg-black py-1 px-2 rounded-md text-white text-xs">New</div>} nonActiveView={<div className="bg-gray-200 py-1 px-2 rounded-md text-xs">New</div>} />
      <SortByButton priority="old" activeView={<div className="bg-black py-1 px-2 rounded-md text-white text-xs">Old</div>} nonActiveView={<div className="bg-gray-200 py-1 px-2 rounded-md text-xs">Old</div>} />
    </div>
  );

  const mobileSection = (
    <Drawer open={isSheetOpen} onOpenChange={(state) => !state && onClose()}>
      <DrawerContent className="h-screen overflow-hidden flex flex-col p-0 pt-6 bg-white gap-3">
        <CommentSectionProvider>
          {sortByOptions}
          <ScrollArea className="flex-1 bg-white">
            <CommentsFeed />
          </ScrollArea>
          <div className="border-t">{isSheetOpen && <NewCommentForm />}</div>
        </CommentSectionProvider>
      </DrawerContent>
    </Drawer>
  );

  const desktopSection = (
    <Sheet open={isSheetOpen} onOpenChange={(state) => !state && onClose()}>
      <SheetContent className="h-screen overflow-hidden flex flex-col p-0 pt-6 bg-white">
        <CommentSectionProvider>
          {sortByOptions}
          <ScrollArea className="flex-1 bg-white">
            <CommentsFeed />
          </ScrollArea>
          <div className="border-t">{isSheetOpen && <NewCommentForm />}</div>
        </CommentSectionProvider>
      </SheetContent>
    </Sheet>
  );

  return <div className="relative">{isDesktop ? desktopSection : mobileSection}</div>;
}

export default DiscussionSheet;
Enter fullscreen mode Exit fullscreen mode

Explanation of DiscussionSheet Component

  1. useEntity - This hook provides the entity context, which in our app is a "post". The connection between Replyke and our posts will be clear once we integrate this component into our App.
  2. useSocialStyle - This hook is required to pass a style configuration object, even if we are using the default styles.
  3. Callbacks - The loginRequiredCallback function notifies users when authentication is needed before interacting. There are additional callbacks available to further customize the behavior of the comment section.
  4. useSocialComments - This hook generates the building blocks for the comment section, including the provider, sorting buttons, and comment components.
  5. SortByButton - These buttons allow sorting comments by "Top", "New", and "Old".
  6. Responsive Layout - The component conditionally renders a Drawer for mobile and a Sheet for desktop, using useMediaQuery.

Updating the App Component

The final step is integrating DiscussionSheet in App.tsx. Replace its content with:

import { useState } from "react";
import { EntityProvider, ReplykeProvider } from "@replyke/react-js";
import { Toaster } from "@/components/ui/sonner";
import { posts } from "./constants/dummy-data";
import { Button } from "./components/ui/button";
import DiscussionSheet from "./components/DiscussionSheet";

function App() {
  const [selectedPostId, setSelectdPostId] = useState<string | null>(null);

  return (
    <ReplykeProvider projectId={import.meta.env.VITE_PUBLIC_REPLYKE_PROJECT_ID}>
      <Toaster />
      <EntityProvider referenceId={selectedPostId} createIfNotFound>
        <DiscussionSheet
          isSheetOpen={!!selectedPostId}
          onClose={() => setSelectdPostId(null)}
        />
      </EntityProvider>
      <div className="h-screen grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 p-6 sm:p-8 md:p-12 lg:p-24 xl:p-52 gap-4 md:gap-8 lg:gap-12 m-auto">
        {posts.map((post) => (
          <div className="bg-white p-4 mb-4 rounded-xl shadow-lg" key={post.id}>
            {/* Post Content */}
            <p className="text-lg font-bold text-gray-800">{post.id}</p>
            <p className="text-gray-600 mt-2">{post.content}</p>

            {/* Open Discussion Button */}
            <Button
              onClick={() => setSelectdPostId(post.id)}
              className="w-full mt-4 cursor-pointer"
            >
              Open Discussion
            </Button>
          </div>
        ))}
      </div>
    </ReplykeProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Explanation of App Component Changes

  • selectedPostId - This state determines which post’s comment section is open.
  • EntityProvider - Wraps DiscussionSheet, linking the Replyke comment system to our posts.
  • referenceId - Instead of entityId, we use referenceId, allowing Replyke to link our existing post IDs to its entities.
  • createIfNotFound - Ensures an entity is created if it doesn’t exist.
  • Open Discussion Button - Clicking it sets selectedPostId, opening the discussion sheet.

Now, clicking “Open Discussion” will open the comment section for that specific post!

Part 4 completed source code on GitHub for reference.

Part 5: Integrating Authentication with Replyke

At the end of Part 4, we successfully set up a functional comment section, but we couldn't interact with it because we weren't authenticated. Replyke's comment section requires information about the logged-in user to function properly. This final part focuses on integrating authentication into our app, enabling users to leave comments and interact with the system as authenticated users.

Understanding Replyke's Authentication

Replyke offers a simple yet robust mechanism to integrate authentication. Here's how it works:

  • JWT Keys: Replyke requires project owners to generate a JWT key pair via the Replyke dashboard. This generates:

    • A secret key (only visible once) for signing JWTs.
    • A public key, which Replyke keeps and associates with the project for verification.
  • JWT Signing: Developers use the secret key to sign a JWT with logged-in user details. This JWT is passed to Replyke to verify its validity and trustworthiness.

  • Authenticated Actions: Once verified, Replyke associates the actions (comments, likes, etc.) with the authenticated user.

Critical Note on Security

  • Never (NEVER) store or use the secret key on the client side in production. Signing JWTs client-side is a major security risk.
  • In production, always implement JWT signing on your server. Replyke’s documentation provides detailed guidance on this.
  • For this tutorial, we’ll demonstrate a simplified implementation for testing and educational purposes only, using Replyke’s useSignTestingJwt hook.

Setting Up Environment Variables

  1. Add your secret key as an environment variable in Expo:

    EXPO_PUBLIC_REPLYKE_SECRET_KEY=your-secret-key
    EXPO_PUBLIC_REPLYKE_PROJECT_ID=your-project-id
    
  2. Use these variables in your src/App.tsx file for authentication setup.

Updated src/App.tsx

import { useEffect, useState } from "react";
import {
  EntityProvider,
  ReplykeProvider,
  useSignTestingJwt,
} from "@replyke/react-js";
import { Toaster } from "@/components/ui/sonner";
import { posts, users } from "./constants/dummy-data";
import { Button } from "./components/ui/button";
import DiscussionSheet from "./components/DiscussionSheet";

const PROJECT_ID = import.meta.env.VITE_PUBLIC_REPLYKE_PROJECT_ID;
const PRIVATE_KEY = import.meta.env.VITE_PUBLIC_REPLYKE_SECRET_KEY;

function App() {
  const signTestingJwt = useSignTestingJwt();

  const [selectedPostId, setSelectdPostId] = useState<string | null>(null);
  const [signedToken, setSignedToken] = useState<string>();

  useEffect(() => {
    const handleSignJwt = async () => {
      const payload = users[0];

      const token = await signTestingJwt({
        projectId: PROJECT_ID,
        payload,
        privateKey: PRIVATE_KEY,
      });
      // Set the signed JWT in the state
      setSignedToken(token);
    };

    handleSignJwt();
  }, []);

  return (
    <ReplykeProvider projectId={PROJECT_ID} signedToken={signedToken}>
      <Toaster />
      <EntityProvider referenceId={selectedPostId} createIfNotFound>
        <DiscussionSheet
          isSheetOpen={!!selectedPostId}
          onClose={() => setSelectdPostId(null)}
        />
      </EntityProvider>
      <div className="h-screen grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 p-6 sm:p-8 md:p-12 lg:p-24 xl:p-52 gap-4 md:gap-8 lg:gap-12 m-auto">
        {posts.map((post) => (
          <div className="bg-white p-4 mb-4 rounded-xl shadow-lg" key={post.id}>
            {/* Post Content */}
            <p className="text-lg font-bold text-gray-800">{post.id}</p>
            <p className="text-gray-600 mt-2">{post.content}</p>

            {/* Open Discussion Button */}
            <Button
              onClick={() => setSelectdPostId(post.id)}
              className="w-full mt-4 cursor-pointer"
            >
              Open Discussion
            </Button>
          </div>
        ))}
      </div>
    </ReplykeProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Logging Out

When a user signs out, we need to notify Replyke to clear the authenticated state. Use the signOut function from the useAuth hook:

import { useAuth } from "@replyke/react-js";

const { signOut } = useAuth();
signOut?.();
Enter fullscreen mode Exit fullscreen mode

This function should be called whenever a user logs out, and it is only available within the ReplykeProvider context.

Key Rotation

If you’ve tested this implementation and later move JWT signing to the server, rotate your secret keys immediately to ensure security. Any key exposed on the client side is compromised and should not be used in production. Regardless, it is a good habit to rotate keys periodically.

Reports and Moderation Tools

Replyke also provides built-in reporting and moderation tools to ensure a safe and respectful community. If a user long-presses on a comment by another user, they can report it. Any reported comment will appear in the project’s dashboard on Replyke for moderation. This seamless integration allows developers to monitor and manage user-generated content effectively.


With this setup, your app can now handle authenticated interactions with Replyke's comment system, providing users with a seamless experience while maintaining a secure foundation. From here, you can explore further customization and server-side integrations for a production-ready implementation.

Part 5 completed source code on GitHub for reference.

Bonus Parts

This article doesn’t end here. In two bonus parts, we will explore how to:

  1. Add GIF Functionality: Enhance user interactions by allowing them to post comments with GIFs, making conversations more engaging and lively.

  2. Add Likes to Posts: Implement a simple and efficient like functionality for posts, encouraging user interaction and providing instant feedback.

Both features are super easy to implement using Replyke’s tools. Let’s dive into them next!


Bonus Part 1: Adding GIF Functionality

Enhancing user interactions with GIFs is incredibly easy using Replyke. Here's how:

  1. Get a GIPHY API Key: Go to GIPHY and create a new API key.

  2. Enable GIFs in Replyke:

  • Copy your API key.

  • Go to your project dashboard on Replyke.

  • Activate the GIF feature and paste your API key.

That's it! GIF support will now be enabled in your comment section, allowing users to search and post GIFs directly.


Bonus Part 2: Adding Likes to Posts

Implementing likes for posts is just as straightforward. Here's how:

  1. Create a SinglePost Component: Create a new SinglePost component in your components directory. It will hold our previous UI logic for each post plus newly added buttons for voting.
import { useEntity, useUser } from "@replyke/react-js";
import { toast } from "sonner";
import { Button } from "./ui/button";
import { cn } from "../lib/utils";

const SinglePost = ({
  handleOpenDiscussionSheet,
}: {
  handleOpenDiscussionSheet: () => void;
}) => {
  const { user } = useUser();
  const {
    entity,
    userDownvotedEntity,
    userUpvotedEntity,
    upvoteEntity,
    removeEntityUpvote,
    downvoteEntity,
    removeEntityDownvote,
  } = useEntity();

  const upvotesCount = entity?.upvotes.length || 0;
  const downvotesCount = entity?.downvotes.length || 0;

  const handleUpvote = () => {
    if (!user) return toast("Please login first");
    if (userUpvotedEntity) {
      removeEntityUpvote?.();
    } else {
      upvoteEntity?.();
    }
  };

  const handleDownvote = () => {
    if (!user) return toast("Please login first");

    if (userDownvotedEntity) {
      removeEntityDownvote?.();
    } else {
      downvoteEntity?.();
    }
  };

  return (
    <div className="bg-white p-4 mb-4 rounded-xl shadow-lg">
      {/* Post Content */}
      <p className="text-lg font-bold text-gray-800">{entity?.referenceId}</p>
      <p className="text-gray-600 mt-2">{entity?.content}</p>

      {/* Voting Section */}
      <div className="grid grid-cols-2 gap-2 items-center mt-4">
        {/* Upvote Button */}
        <Button
          onClick={handleUpvote}
          size="sm"
          className={cn(
            "cursor-pointer text-xs",
            userUpvotedEntity
              ? "bg-green-500 hover:bg-green-500  text-white"
              : "bg-gray-200 hover:bg-gray-200 text-gray-800"
          )}
        >
          {userUpvotedEntity ? "Upvoted" : "Upvote"}
          <span>({upvotesCount})</span>
        </Button>

        {/* Downvote Button */}
        <Button
          onClick={handleDownvote}
          size="sm"
          className={cn(
            "cursor-pointer text-xs",
            userDownvotedEntity
              ? "bg-red-500 hover:bg-red-500 text-white"
              : "bg-gray-200 hover:bg-gray-200 text-gray-800"
          )}
        >
          {userDownvotedEntity ? "Downvoted" : "Downvote"}
          <span>({downvotesCount})</span>
        </Button>
      </div>

      {/* Open Discussion Button */}
      <Button
        onClick={handleOpenDiscussionSheet}
        className="w-full mt-4 cursor-pointer"
      >
        Open Discussion{" "}
        {(entity?.repliesCount || 0) > 0 && `(${entity?.repliesCount})`}
      </Button>
    </div>
  );
};

export default SinglePost;
Enter fullscreen mode Exit fullscreen mode
  1. Wrap Each Post with an EntityProvider: Update the src/App.tsx file to return the new SinglePost component, with each post wrapped in an EntityProvider.
import { useEffect, useState } from "react";
import {
  EntityProvider,
  ReplykeProvider,
  useSignTestingJwt,
} from "@replyke/react-js";
import { Toaster } from "@/components/ui/sonner";
import { posts, users } from "./constants/dummy-data";
import DiscussionSheet from "./components/DiscussionSheet";
import SinglePost from "./components/SinglePost";

const PROJECT_ID = import.meta.env.VITE_PUBLIC_REPLYKE_PROJECT_ID;
const PRIVATE_KEY = import.meta.env.VITE_PUBLIC_REPLYKE_SECRET_KEY;

function App() {
  const signTestingJwt = useSignTestingJwt();

  const [selectedPostId, setSelectdPostId] = useState<string | null>(null);
  const [signedToken, setSignedToken] = useState<string>();

  useEffect(() => {
    const handleSignJwt = async () => {
      const payload = users[0];

      const token = await signTestingJwt({
        projectId: PROJECT_ID,
        payload,
        privateKey: PRIVATE_KEY,
      });
      // Set the signed JWT in the state
      setSignedToken(token);
    };

    handleSignJwt();
  }, []);

  return (
    <ReplykeProvider projectId={PROJECT_ID} signedToken={signedToken}>
      <Toaster />
      <EntityProvider referenceId={selectedPostId} createIfNotFound>
        <DiscussionSheet
          isSheetOpen={!!selectedPostId}
          onClose={() => setSelectdPostId(null)}
        />
      </EntityProvider>
      <div className="h-screen grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 p-6 sm:p-8 md:p-12 lg:p-24 xl:p-52 gap-4 md:gap-8 lg:gap-12 m-auto">
        {posts.map((post) => (
          <EntityProvider referenceId={post.id} key={post.id}>
            <SinglePost
              handleOpenDiscussionSheet={() => setSelectdPostId(post.id)}
            />
          </EntityProvider>
        ))}
      </div>
    </ReplykeProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

That's it! Your posts now support upvotes and downvotes.

Part 6 completed source code on GitHub for reference.


Wrapping Up

In this article, we’ve covered setting up a fully functional comment section, adding authentication, enabling GIFs, and implementing voting functionality for posts. These features were straightforward to integrate, showcasing Replyke’s power and simplicity.

If you found this interesting, explore the rest of what Replyke has to offer, including advanced feeds, app notifications, user-generated lists, profiles, and much more. All are as easy to implement as the features we’ve covered here!

Stay Updated

Don’t forget to join the Discord server where I post free boilerplate code repos for different types of social networks. Updates about the article tutorials and additional resources will also be shared there. Lastly, follow me for updates here and on X/Twitter & BlueSky.

Top comments (0)