DEV Community

Cover image for 🪄✨Building a blog with a liking feature using React, Hanko and Novu 🔥
Nevo David Subscriber for novu

Posted on

🪄✨Building a blog with a liking feature using React, Hanko and Novu 🔥

TL;DR 🔥

In this tutorial, you'll learn how to build a blogging platform that let's you create and react to posts.

  • We will build a login and registration with Hanko
  • Build the entire blog:
    • Create posts
    • React to posts
  • Add in-app notification to every reaction with Novu.

Blog


Novu: Open-source notification infrastructure 🚀

Just a quick background about us. Novu is an open-source notification infrastructure. We basically help to manage all the product notifications. It can be In-App (the bell icon like you have in the Dev Community - Websockets), Emails, SMSs and so on.

Like


Let set it up 🆙

Here, I'll walk you through creating the project setup for the application. We'll use React.js for the front end and Node.js for the backend server.

Create a folder for the web application as done below.



mkdir simple-blog
cd simple-blog
mkdir client server


Enter fullscreen mode Exit fullscreen mode

Setting up the Node.js server

Navigate into the server folder and create a package.json file.



cd server & npm init -y


Enter fullscreen mode Exit fullscreen mode

Install Express, Nodemon, and the CORS library.



npm install express cors nodemon


Enter fullscreen mode Exit fullscreen mode

ExpressJS is a fast, minimalist framework that provides several features for building web applications in Node.js, CORS is a Node.js package that allows communication between different domains, and Nodemon is a Node.js tool that automatically restarts the server after detecting file changes.

Create an index.js file - the entry point to the web server.



touch index.js


Enter fullscreen mode Exit fullscreen mode

Set up a Node.js server using Express.js. The code snippet below returns a JSON object when you visit the http://localhost:4000/api in your browser.



//👇🏻index.js
const express = require("express");
const cors = require("cors");
const app = express();
const PORT = 4000;

app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cors());

app.get("/api", (req, res) => {
    res.json({
        message: "Hello world",
    });
});

app.listen(PORT, () => {
    console.log(`Server listening on ${PORT}`);
});


Enter fullscreen mode Exit fullscreen mode

Configure Nodemon by adding the start command to the list of scripts in the package.json file. The code snippet below starts the server using Nodemon.



//In server/package.json

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon index.js"
  },


Enter fullscreen mode Exit fullscreen mode

Congratulations!🎉 You can now start the server by using the command below.



npm start


Enter fullscreen mode Exit fullscreen mode

Setting up the React application

Navigate into the client folder via your terminal and create a new React.js project with Vite.



npm create vite@latest


Enter fullscreen mode Exit fullscreen mode

Install React Icons and React Router - a JavaScript library that enables us to navigate between pages in a React application.



npm install react-router-dom react-icons


Enter fullscreen mode Exit fullscreen mode

Delete the redundant files, such as the logo and the test files from the React app, and update the App.jsx file to display “Hello World” as done below.



function App() {
    return (
        <div>
            <p>Hello World!</p>
        </div>
    );
}
export default App;


Enter fullscreen mode Exit fullscreen mode

Copy the CSS file required for styling the project here into the src/index.css file.


Building the app user interface 🛠️

Here, we'll create the user interface for the blogging application to enable users to view, create, and react to posts.

Create a components folder within the client/src folder containing the Home.jsx, Login.jsx, Details.jsx, and NewPost.jsx.



cd client/src
mkdir components
touch Home.jsx Details.jsx Login.jsx NewPost.jsx


Enter fullscreen mode Exit fullscreen mode
  • From the code snippet above
    • The Home.jsx component displays all the available posts.
    • The Detail.jsx component displays the details of each post, such as its content, the date posted, and the number of reactions to the post.
    • The NewPost.jsx component enables users to create a new post.
    • The Login.jsx component log users into the application via Hanko.

Update the App.jsx file to render the components using React Router.



import React from "react";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import Home from "./components/Home";
import Details from "./components/Details";
import Login from "./components/Login";
import NewPost from "./components/NewPost";

const App = () => {
    return (
        <Router>
            <Routes>
                <Route path='/' element={<Home />} />
                <Route path='/login' element={<Login />} />
                <Route path='/post/:slug' element={<Details />} />
                <Route path='/post/new' element={<NewPost />} />
            </Routes>
        </Router>
    );
};

export default App;


Enter fullscreen mode Exit fullscreen mode

The Home page

The Home page displays all the posts created within the application. Copy the code below into the Home.jsx file.



import React from "react";
import { Link } from "react-router-dom";

const Home = () => {
    return (
        <div>
            <nav className='navbar'>
                <Link to='/' className='logo'>
                    <h2>MyBlog</h2>
                </Link>
                <div style={{ display: "flex", alignItems: "center" }}>
                    <Link to='/post/new' className='newPostBtn'>
                        New Post
                    </Link>
                </div>
            </nav>
            <main className='main'>
                <h2 className='heading'>Latest Posts</h2>
                <div className='posts_container'>
                    <Link to={`/post/details`} className='post'>
                        <h2 className='post_title'>
                            Building a chat app with React, Novu, and Websockets
                        </h2>
                    </Link>

                    <Link to={`/post/details`} className='post'>
                        <h2 className='post_title'>How to install Novu in React</h2>
                    </Link>
                </div>
            </main>
        </div>
    );
};

export default Home;


Enter fullscreen mode Exit fullscreen mode

Home Page

The Post Details page

This page displays post details when a user clicks on them from the Home.jsx component. Copy the code below into the Details.jsx file.



import React from "react";
import { AiTwotoneLike, AiTwotoneDislike } from "react-icons/ai";

const Details = () => {
    return (
        <div>
            <header className='details_header'>
                <h1 className='details_heading'>How to install Novu in React</h1>
                <div className='post_details'>
                    <div>
                        <p className='details_date'>Posted on 30th July, 2023</p>
                    </div>
                    <div className='reactions-group'>
                        <button className='reactBtn'>
                            Like <AiTwotoneLike /> <span style={{ marginLeft: 5 }}>2</span>
                        </button>
                        <button className='reactBtn unlikeBtn'>
                            Dislike <AiTwotoneDislike />
                            <span style={{ marginLeft: 5 }}>1</span>
                        </button>
                    </div>
                </div>
            </header>
            <main className='details_body'>
                Lorem Ipsum is simply dummy text of the printing and typesetting
                industry. Lorem Ipsum has been the industry's standard dummy text ever
                since the 1500s, when an unknown printer took a galley of type and
                scrambled it to make a type specimen book. It has survived not only five
                centuries, but also the leap into electronic typesetting, remaining
                essentially unchanged. It was popularised in the 1960s with the release
                of Letraset sheets containing Lorem Ipsum passages, and more recently
                with desktop publishing software like Aldus PageMaker including versions
                of Lorem Ipsum.
            </main>
        </div>
    );
};

export default Details;


Enter fullscreen mode Exit fullscreen mode

The Post Details page

The New Post page

This page displays a form field that accepts the title and content of a blog post. Copy the code snippet below into the NewPost.jsx file.



import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";

const NewPost = () => {
    const navigate = useNavigate();
    const [title, setTitle] = useState("");
    const [content, setContent] = useState("");

    const handleSubmit = (e) => {
        e.preventDefault();
        console.log({ title, content });
        setContent("");
        setTitle("");
    };
    return (
        <div>
            <nav className='navbar'>
                <Link to='/' className='logo'>
                    <h2>MyBlog</h2>
                </Link>

                <div>
                    <button className='newPostBtn logOut'>Log out</button>
                </div>
            </nav>
            <main className='main'>
                <h2 className='heading'>Create new post</h2>
                <form className='newPost_form' onSubmit={handleSubmit}>
                    <label htmlFor='title' className='label'>
                        Title
                    </label>
                    <input
                        type='text'
                        className='newPost_title'
                        id='title'
                        name='title'
                        value={title}
                        required
                        onChange={(e) => setTitle(e.target.value)}
                    />
                    <label htmlFor='content' className='label'>
                        Content
                    </label>
                    <textarea
                        rows={10}
                        className='newPost_content'
                        value={content}
                        required
                        onChange={(e) => setContent(e.target.value)}
                    />
                    <button className='newPostBtn submitBtn' type='submit'>
                        Create Post
                    </button>
                </form>
            </main>
        </div>
    );
};

export default NewPost;


Enter fullscreen mode Exit fullscreen mode

The New Post page


Are passkeys the future? 🔑

Hanko is an open-source, easy-to-integrate authentication solution that enables you to add various forms of authentication such as Email & Password, password-less, passkeys, and OAuth to your software applications.

It is an all-in-one authentication solution that enables you to set up authentication in a few minutes in your web applications. It also provides customisable web components which you can add to your web application to handle authentication quickly and easily.

In the upcoming sections, you'll learn how to add Hanko to the blogging application.

Adding authentication easily to React apps with Hanko

Here, you'll learn how to add authentication to your React applications using Hanko. Before we begin, install the Hanko package by running the code snippet below.



npm install @teamhanko/hanko-elements


Enter fullscreen mode Exit fullscreen mode

Setting up an Hanko project

Visit the homepage and create an account.

Create a new organization that will manage your Hanko projects.

Setting up an Hanko project

Then, create a new Hanko project and add your development server as the API URL.

new Hanko project

Finally, save your API URL somewhere on your computer; it will be used for setting up the authentication.

API URL

Adding Hanko to React apps

Copy the code below into the Login.jsx file.



import React, { useEffect, useCallback, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { register, Hanko } from "@teamhanko/hanko-elements";
const hankoApi = "<YOUR_HANKO_API_URL>";

const Login = () => {
    const navigate = useNavigate();
    const hanko = useMemo(() => new Hanko(hankoApi), []);

    useEffect(() => {
        register(hankoApi).catch((error) => {
            console.log(error);
        });
    }, []);

    return (
        <div className='login_container'>
            <hanko-auth />
        </div>
    );
};

export default Login;


Enter fullscreen mode Exit fullscreen mode

The code snippet displays the Hanko authentication component and enables users to sign up or sign in directly via Hanko.

code snippet

Add the code snippet below within the Login.jsx component.



//👇🏻 generates random string as ID
const generateUserID = () => Math.random().toString(36).substring(2, 10);

//👇🏻 executes after a user logs in
const redirectAfterLogin = useCallback(() => {
    localStorage.setItem("loggedIn", "true");
    if (!localStorage.getItem("u_id")) {
        localStorage.setItem("u_id", generateUserID());
    }
    navigate("/");
}, [navigate]);

//👇🏻 triggered after a successful sign in
useEffect(
    () =>
        hanko.onAuthFlowCompleted(() => {
            redirectAfterLogin();
        }),
    [hanko, redirectAfterLogin]
);


Enter fullscreen mode Exit fullscreen mode

From the code snippet above, when a user signs into the application, the u_id value is set to the local storage to identify each user when they request the Node.js server.

💡PS: I'm using local storage because this is a small application. If you are using Hanko in a production environment, you may need to check out the backend guide.

Congratulations!🎉 You've successfully added Hanko to a React application. In the upcoming section, we'll add all the necessary features to the blogging application.

Communicating with the Node.js server

In this section, you'll learn how to communicate with the Node.js server by retrieving and creating posts within the application.

Before we begin, create a utils folder containing a util.js file within the React app.



cd client
mkdir utils
cd utils
touch util.js


Enter fullscreen mode Exit fullscreen mode

Displaying the blog posts

Create a posts array within the index.js file on the server.



let posts = [
    {
        u_id: "a123",
        post_id: "1",
        title: "Building a chat app with NextJS and Novu",
        slug: "building-a-chat-app-with-nextjs-and-novu",
        content:
            "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.",
        published_date: "27-07-2023",
        likes: [{ u_id: "12345" }, { u_id: "ancsd" }],
        dislikes: [{ user_id: "12345" }, { u_id: "12345" }],
    },
    {
        u_id: "b123",
        post_id: "2",
        title: "How to create an ecommerce app with NextJS and Novu ",
        slug: "how-to-create-an-ecommerce-app-with-nextjs-and-novu",
        content:
            "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets.",
        published_date: "27-07-2023",
        likes: [{ u_id: "12345" }],
        dislikes: [{ user_id: "12345" }],
    },
];


Enter fullscreen mode Exit fullscreen mode

Add another endpoint that returns the posts in JSON format.



app.get("/posts", (req, res) => {
    res.json({
        posts,
    });
});


Enter fullscreen mode Exit fullscreen mode

Next, create a function within the utils/util.js file that sends a request to the endpoint from the React app.



export const fetchAllPosts = (setLoading, setPosts) => {
    fetch("http://localhost:4000/posts")
        .then((res) => res.json())
        .then((data) => {
            setLoading(false);
            setPosts(data.posts);
        })
        .catch((err) => console.error(err));
};


Enter fullscreen mode Exit fullscreen mode

Finally, execute the function when the Home component mounts.



import React, { useCallback, useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { fetchAllPosts } from "../utils/util";

const Home = () => {
    const [loggedIn, setLoggedIn] = useState(false);
    const [posts, setPosts] = useState([]);
    const [loading, setLoading] = useState(true);

    const fetchPosts = useCallback(() => {
        fetchAllPosts(setLoading, setPosts);
    }, []);

    useEffect(() => {
        if (localStorage.getItem("loggedIn")) {
            setLoggedIn(true);
        }
        fetchPosts();
    }, [fetchPosts]);

    if (loading) return <p>Loading...</p>;

    return (
        <div>
            <nav className='navbar'>
                <Link to='/' className='logo'>
                    <h2>MyBlog</h2>
                </Link>
                <div style={{ display: "flex", alignItems: "center" }}>
                    {loggedIn ? (
                        <Link to='/post/new' className='newPostBtn'>
                            New Post
                        </Link>
                    ) : (
                        <Link to='/login' className='newPostBtn'>
                            Log in
                        </Link>
                    )}
                </div>
            </nav>
            <main className='main'>
                <h2 className='heading'>Latest Posts</h2>
                <div className='posts_container'>
                    {posts?.map((post) => (
                        <Link to={`/post/${post.slug}`} className='post' key={post.post_id}>
                            <h2 className='post_title'>{post.title}</h2>
                        </Link>
                    ))}
                </div>
            </main>
        </div>
    );
};

export default Home;


Enter fullscreen mode Exit fullscreen mode

The code snippet above fetches all the posts from the server when the page mounts and displays them within the React app. It also checks if the user is authenticated to display either the Login or New Post buttons.

Retrieving the posts' details

Here, you need to fetch a post's details when you click on it from the Home page. To do this, you need to filter the array of posts via its slug property.

Create another POST route that filters the posts array by a post's slug and returns the entire post object.



app.post("/post/details", (req, res) => {
    const { slug } = req.body;
    const result = posts.filter((post) => post.slug === slug);
    res.json({ post: result[0] });
});


Enter fullscreen mode Exit fullscreen mode

Add a function within the utils/util.js file that sends a request to the post/details endpoint and returns the post object.



export const fetchPostContent = (slug, setLoading, setPost) => {
    fetch("http://localhost:4000/post/details", {
        method: "POST",
        body: JSON.stringify({ slug: slug }),
        headers: {
            Accept: "application/json",
            "Content-Type": "application/json",
        },
    })
        .then((res) => res.json(res))
        .then((data) => {
            setLoading(false);
            setPost(data.post);
        })
        .catch((err) => console.error(err));
};


Enter fullscreen mode Exit fullscreen mode

Import the function into the Details.jsx component.



import { useParams } from "react-router-dom";
import { fetchPostContent } from "../utils/util";

const Details = () => {
    const { slug } = useParams();
    const [post, setPost] = useState({});
    const [loading, setLoading] = useState(true);

    const fetchPostDetails = useCallback(() => {
        fetchPostContent(slug, setLoading, setPost)
    }, [slug]);

    useEffect(() => {
        fetchPostDetails();
    }, [fetchPostDetails]);

    if (loading) return <p>Loading...</p>;

    return (
        <div>....</div>
    )


Enter fullscreen mode Exit fullscreen mode

Update the UI elements to display the post details accordingly.



return (
    <div>
        <header className='details_header'>
            <h1 className='details_heading'>{post.title}</h1>
            <div className='post_details'>
                <div>
                    <p className='details_date'>Posted on {post.published_date}</p>
                </div>
                <div className='reactions-group'>
                    <button
                        className='reactBtn'
                        onClick={() => reactToPost(slug, "like")}
                    >
                        Like <AiTwotoneLike />{" "}
                        <span style={{ marginLeft: 5 }}>{post.likes.length}</span>
                    </button>
                    <button
                        className='reactBtn unlikeBtn'
                        onClick={() => reactToPost(slug, "dislike")}
                    >
                        Dislike <AiTwotoneDislike />
                        <span style={{ marginLeft: 5 }}>{post.dislikes.length}</span>
                    </button>
                </div>
            </div>
        </header>
        <main className='details_body'>{post.content}</main>
    </div>
);


Enter fullscreen mode Exit fullscreen mode

Like Button

Reacting to blog posts

First, you need to create an endpoint on the Node.js server that updates the number of likes and dislikes property of a post when a user clicks the button from the user interface.



app.post("/post/react", async (req, res) => {
    const { slug, type, u_id } = req.body;

    //👇🏻 like post functionality
    for (let i = 0; i < posts.length; i++) {
        if (posts[i].slug === slug && type === "like") {
            //👇🏻 validates the post reaction
            const validateLike = posts[i].likes.filter(
                (likes) => likes.u_id === u_id
            );
            if (validateLike.length === 0) {
                posts[i].likes.push({ u_id });
                res.json({ message: "You've just liked a post" });
            }
        }

        //👇🏻 dislike post functionality
        if (posts[i].slug === slug && type === "dislike") {
            //👇🏻 validates the post reaction
            const validateDislike = posts[i].dislikes.filter(
                (dislikes) => dislikes.u_id === u_id
            );
            if (validateDislike.length === 0) {
                posts[i].dislikes.push({ u_id });
                const sendNotifcation = await notify("liked", u_id);
                res.json({ message: "You've just disliked a post" });
            }
        }
    }
});


Enter fullscreen mode Exit fullscreen mode

The code snippet above handles the user's reaction to posts. It filters the posts array via the post's slug and validates the post reaction to ensure that the user has not reacted to the post, before updating the property accordingly.

Create a function within the utils/util.js file that sends a request to the endpoint when a user clicks the Like and Dislike buttons.



export const postReaction = (slug, type) => {
    fetch("http://localhost:4000/post/react", {
        method: "POST",
        body: JSON.stringify({ slug, type, u_id: localStorage.getItem("u_id") }),
        headers: {
            Accept: "application/json",
            "Content-Type": "application/json",
        },
    })
        .then((res) => res.json(res))
        .then((data) => alert(data.message))
        .catch((err) => console.error(err));
};


Enter fullscreen mode Exit fullscreen mode

Execute the function when a user clicks on the buttons.



import { postReaction } from "../utils/util";

const Details = () => {
    //👇🏻 calls the function
    const reactToPost = (slug, type) => {
        postReaction(slug, type);
    };

    return (
        <div>
            <header className='details_header'>
                <h1 className='details_heading'>{post.title}</h1>
                <div className='post_details'>
                    <div>
                        <p className='details_date'>Posted on {post.published_date}</p>
                    </div>
                    <div className='reactions-group'>
                        {/*-- like button*/}
                        <button
                            className='reactBtn'
                            onClick={() => reactToPost(slug, "like")}
                        >
                            Like <AiTwotoneLike />{" "}
                            <span style={{ marginLeft: 5 }}>{post.likes.length}</span>
                        </button>
                        {/*-- Dislike button*/}
                        <button
                            className='reactBtn unlikeBtn'
                            onClick={() => reactToPost(slug, "dislike")}
                        >
                            Dislike <AiTwotoneDislike />
                            <span style={{ marginLeft: 5 }}>{post.dislikes.length}</span>
                        </button>
                    </div>
                </div>
            </header>
            <main className='details_body'>{post.content}</main>
        </div>
    );
};
export default Details;


Enter fullscreen mode Exit fullscreen mode

Creating new posts

Create an endpoint that adds a new post to the posts array.



//👇🏻 creates post slug
const createSlug = (text, id) => {
    let slug = text
        .trim()
        .toLowerCase()
        .replace(/[^\w\s-]/g, "");
    slug = slug.replace(/\s+/g, "-");
    return slug + "-" + id;
};

//👇🏻 generates a random string as ID
const generateID = () => Math.random().toString(36).substring(2, 10);

app.post("/post/add", (req, res) => {
    const { u_id, title, content, date } = req.body;
    const postObject = {
        u_id,
        post_id: generateID(),
        title,
        slug: createSlug(title, generateID()),
        content,
        published_date: date,
        likes: [],
        dislikes: [],
    };
    posts.unshift(postObject);
    res.json({ message: "Post added successfully!✅" });
});


Enter fullscreen mode Exit fullscreen mode

The code snippet above creates a new post object and adds the newly created post to the posts array.

Add a function that sends a request to the endpoint within the utils/util.js file.



export const addNewPost = (u_id, title, content, date, navigate) => {
    fetch("http://localhost:4000/post/add", {
        method: "POST",
        body: JSON.stringify({ u_id, title, content, date }),
        headers: {
            Accept: "application/json",
            "Content-Type": "application/json",
        },
    })
        .then((res) => res.json(res))
        .then((data) => {
            alert(data.message);
            navigate("/");
        })
        .catch((err) => {
            console.error(err);
            alert("Encountered an error ❌");
        });
};


Enter fullscreen mode Exit fullscreen mode

Execute the function when the user submits the form within the NewPost.jsx file.



//👇🏻 formates the date to a readable string
const formatDate = () => {
    const date = new Date();
    const day = String(date.getDate()).padStart(2, "0");
    const month = String(date.getMonth() + 1).padStart(2, "0");
    const year = date.getFullYear();
    return `${day}-${month}-${year}`;
};

//👇🏻 executes on form submit
const handleSubmit = (e) => {
    e.preventDefault();
    //👇🏻 adds the new post
    addNewPost(
        localStorage.getItem("u_id"),
        title,
        content,
        formatDate(),
        navigate
    );
    setContent("");
    setTitle("");
};


Enter fullscreen mode Exit fullscreen mode

Sending in-app notifications with Novu 📳

Here, we need to notify the post authors when someone reacts to their posts. To do this, we'll use Novu - an open-source notification infrastructure that enables you to send in-app, SMS, chat, push, and e-mail notifications from a single dashboard.

Creating a Novu project

Navigate into the client folder and create a Novu project by running the code below.



cd client
npx novu init


Enter fullscreen mode Exit fullscreen mode

Select your application name and sign in to your Novu dashboard. The code snippet below contains the steps you should follow after running npx novu init.



Now let's setup your account and send your first notification
? What is your application name? Forum App
? Now lets setup your environment. How would you like to proceed? Create a free cloud account (Recommended)
? Create your account with: Sign-in with GitHub
? I accept the Terms and Conditions (https://novu.co/terms) and have read the Privacy Policy (https://novu.co/privacy) Yes
✔ Created your account successfully.


Enter fullscreen mode Exit fullscreen mode

Visit the demo page, copy your subscriber ID from the page, and click the Skip Tutorial button.

SubId

Create a notification template with a workflow as shown below:

Notification template

Novu Digest allows you to control how you send notifications in your app. It collects multiple trigger events, schedules them, or sends them as a single message.

Update the In-App notification step to send this message to the post author when someone reacts to their post.



You have a new {{reaction}} on your post.


Enter fullscreen mode Exit fullscreen mode

Reaction

Adding Novu notification bell to a React app

Novu in-app notification uses a notification bell to send alerts to users. Here, you'll learn how to add it to your React applications.

Install the Novu Notification package.



npm install @novu/notification-center


Enter fullscreen mode Exit fullscreen mode

Create a Novu.jsx file within the components folder and copy the below into the file.



import React from "react";
import {
    NovuProvider,
    PopoverNotificationCenter,
    NotificationBell,
} from "@novu/notification-center";
import { useNavigate } from "react-router-dom";

function Novu() {
    const navigate = useNavigate();
    const onNotificationClick = (notification) =>
        navigate(notification.cta.data.url);

    return (
        <>
            <NovuProvider
                subscriberId='<YOUR_SUBSCRIBER_ID>'
                applicationIdentifier='<YOUR_APP_ID>'
            >
                <PopoverNotificationCenter
                    onNotificationClick={onNotificationClick}
                    colorScheme='light'
                >
                    {({ unseenCount }) => <NotificationBell unseenCount={unseenCount} />}
                </PopoverNotificationCenter>
            </NovuProvider>
        </>
    );
}

export default Novu;


Enter fullscreen mode Exit fullscreen mode

The code snippet above enables us to add Novu's notification bell icon to the application. With this, you can view all the notifications within the app.
Select Settings on your Novu Admin Panel to copy your App ID and replace the subscriber's ID placeholder with yours.

Import the Novu.jsx component into the Home.jsx component.



const Home = () => {
    return (
        <div>
            <nav className='navbar'>
                <Link to='/' className='logo'>
                    <h2>MyBlog</h2>
                </Link>
                <div style={{ display: "flex", alignItems: "center" }}>
                    {/*---👇🏻 Novu component👇🏻---*/}
                    <Novu />
                    {loggedIn ? (
                        <Link to='/post/new' className='newPostBtn'>
                            New Post
                        </Link>
                    ) : (
                        <Link to='/login' className='newPostBtn'>
                            Log in
                        </Link>
                    )}
                </div>
            </nav>
            {/*--- other components ---*/}
        </div>
    );
};


Enter fullscreen mode Exit fullscreen mode

Novu

Configuring Novu on a Node.js server

Install the Novu SDK for Node.js into the server folder.



npm install @novu/node


Enter fullscreen mode Exit fullscreen mode

Import Novu from the package and create an instance using your API Key.



const { Novu } = require("@novu/node");
const novu = new Novu("<YOUR_API_KEY>");


Enter fullscreen mode Exit fullscreen mode

Create a function within the index.js file that sends notification to the post author via Novu.



const notify = async (reaction, userID) => {
    await novu.subscribers.identify(userID, {
        firstName: "inAppSubscriber",
    });
    const response = await novu.trigger("notify", {
        to: {
            subscriberId: "<YOUR_SUBSCRIBER_ID>",
        },
        payload: {
            reaction,
        },
    });
    return response.data.data;
};


Enter fullscreen mode Exit fullscreen mode

Execute the function when a user reacts to a post.



app.post("/post/react", async (req, res) => {
    const { slug, type, u_id } = req.body;

    for (let i = 0; i < posts.length; i++) {
        if (posts[i].slug === slug && type === "like") {
            const validateLike = posts[i].likes.filter(
                (likes) => likes.u_id === u_id
            );
            if (validateLike.length === 0) {
                posts[i].likes.push({ u_id });
                //👇🏻 Triggers Novu
                const sendNotifcation = await notify("like", u_id);
                if (sendNotifcation.acknowledged) {
                    res.json({ message: "You've just liked a post" });
                }
            }
        }
        if (posts[i].slug === slug && type === "dislike") {
            const validateDislike = posts[i].dislikes.filter(
                (dislikes) => dislikes.u_id === u_id
            );
            if (validateDislike.length === 0) {
                posts[i].dislikes.push({ u_id });
                //👇🏻 Triggers Novu
                const sendNotifcation = await notify("dislike", u_id);
                if (sendNotifcation.acknowledged) {
                    res.json({ message: "You've just disliked a post" });
                }
            }
        }
    }
});


Enter fullscreen mode Exit fullscreen mode

Congratulations! You've completed the application.

Congratulations


Conclusion

So far, you've learnt how to authenticate users with Hanko, communicate between a React and Node.js app, and send in-app notifications using the Novu Digest.

Novu enables you to create a rich notification system in your applications, thereby providing a great user experience for your users. You should also try out Hanko - it is minimal and easy to integrate.

The source code for this tutorial is available here:

https://github.com/novuhq/blog/tree/main/hanko-auth-blog-with-novu.

Thank you for reading!


Like

Top comments (17)

Collapse
 
vincanger profile image
vincanger

really liked the notify feature. novu looks cool!

Collapse
 
nevodavid profile image
Nevo David

Thank you so much Vinny!

Collapse
 
sumitsaurabh927 profile image
Sumit Saurabh

Love this article! 🚀

Collapse
 
nevodavid profile image
Nevo David

Thank you so much Sumit!

Collapse
 
leonavevor profile image
leonavevor

Great job 👍

Collapse
 
matijasos profile image
Matija Sosic

Another detailed tutorial, great job :)

Collapse
 
nevodavid profile image
Nevo David

Thank you so much @matijasos!

Collapse
 
infocampus_2000_cce034c88 profile image
Infocampus 2000

Howdy! I could have sworn I’ve been to your blog before but after browsing through many of the articles I realized it’s new to me. Anyways, I’m certainly happy I found it and I’ll be book-marking it and checking back often!

infocampus.co.in/web-designing-tra...
infocampus.co.in/web-development-t...
infocampus.co.in/front-end-develop...
infocampus.co.in/full-stack-develo...
infocampus.co.in/advanced-java-tra...
infocampus.co.in/best-selenium-tes...

Collapse
 
dev_bre profile image
Andy

very clean and well written article Nevo! I like how you used Hanko, I will check that out, and the integration with Novu is really simple.

Collapse
 
nevodavid profile image
Nevo David

Thank you so much Andy 🙇🏻‍♂️

Collapse
 
yoloz profile image
yoloz

a good idea👍

Collapse
 
firoz786 profile image
firoz alam

great

Collapse
 
williangk profile image
Willian Gabriel

pretty awesome

Collapse
 
fullstackscout profile image
Gary

Dang, that's cool. Thanks 👍