Notification system for a grocery app with React and NodeJS

Mark Abeto - Mar 15 '23 - - Dev Community

TL;DR

In this guide, we’ll make a simple shoping list webapp with Node and Express. Then, we’ll be using Novu as an open source notification system to send email reminders about our groceries on the day we need to get them.

Getting Some Groceries

I’ll admit I’m not the best when it comes to keeping track of things. Half the time, when I go to get groceries, I end up missing a chunk of stuff that totally slipped my mind. While I could easily just write things down, I’m also a dev and like to make things difficult for myself. So let’s throw something together!

Image description

In this article, we’ll be going over a simple shopping list that we’ll create with Node.js, as well as some pointers on how we can use the Novu platform to send emails from our API.

Setting Up Our Project

If you’d like to see the full project Github, you can check it out here.

Naturally, we’ll need to start our project somehow. The first step we need to do is to create a project folder and then add in a basic webpage.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="./styles.css">
  <link rel="stylesheet" href="https://unicons.iconscout.com/release/v4.0.0/css/line.css">
  <script src="https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.0/dist/index.umd.min.js"></script>
  <title>Grocery Notification</title>
</head>
<body>
    <div class="container">
      <div class="input">
        <input name="Enter new item" id="grocery-input"  placeholder="Enter new grocery item"></inp>
        <i class="uil uil-notes notes-icon"></i>
      </div>

      <div class="datepicker-container">
        <input id="datepicker" placeholder="Schedule Grocery Date" type="text"/>
      </div>

      <h1 class="title">Grocery Items</h1>
      <ul class="grocery-list">
        <li class="grocery-list-item" >
          <span class="grocery-item">Eggs</span>
          <i class="uil uil-trash delete-icon"></i>
        </li>
      </ul>

      <button class="submit" type="button">Schedule</button>
    </div>
    <div class="error notification"></div>
    <div class="success notification"></div>
    <script src="./script.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Our markup is pretty simple actually. We have our input where we specify the grocery item we need to add and the list of items. Lastly, we have an input date where we specify the date we want to be reminded.

We’re going to use a third-party date picker library called easepick. It’s a pretty simple library to drop into our app, and we’ll use it alongside icons from Unicons.

We’ll use some styling for our site too, which you can find here:

styles.css

@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,600;1,300&family=Roboto&display=swap');

* {
  font-family: 'Roboto', sans-serif;
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  background-color: #eee;
  height: 100vh;
  padding: 0;

}

.container {
  position: relative;
  max-width: 500px;
  width: 100%;
  background-color: #fff;
  box-shadow: 0 3px 5px rgba(0,0,0,0.1);
  padding: 30px;
  margin: 85px auto;
  border-radius: 6px;
}

.container .input {
  position: relative;
  height: 70px;
  width: 100%
}

.container .datepicker-container {
  margin-top: 15px;
  display: flex;
  justify-content: flex-end;
}

.container input#datepicker {
  padding: 8px;
  outline: none;
  border-radius: 6px;
  border: 1px solid #cecece;
}

input#grocery-input {
  height: 100%;
  width: 100%;
  outline: none;
  border-radius: 6px;
  padding: 25px 18px 18px 18px;
  font-size: 16px;
  font-weight: 400;
  resize: none;
}

.notes-icon {
  position: absolute;
  top: 50%;
  font-size: 15px;
  right: 20px;
  transform: translateY(-50%);
  font-size: 18px;
  color: #828282;
}

.title {
  text-align: center;
  margin-top: 25px;
  margin-bottom: 0px;
}

.grocery-list {
  margin-top: 30px;
}

.grocery-list .grocery-list-item {
  list-style: none;
  display: flex;
  align-items: center;
  width: 100%;
  background-color: #eee;
  padding: 15px;
  border-radius: 6px;
  position: relative;
  margin-top: 15px;
}

.grocery-list .grocery-item {
  margin-left: 15px;
}

.grocery-list .delete-icon {
  position: absolute;
  right: 15px;
  cursor: pointer;
}

button.submit {
  margin-top: 40px;
  padding: 12px;
  border-radius: 6px;
  outline: none;
  border: none;
  width: 100%;
  background-color: #0081C9;
  color: white;
  cursor: pointer;
}

.notification {
  position: absolute;
  top: 5%;
  left: 50%;
  transform: translate(-50%,-50%);
  width: 250px;
  height: auto;
  padding: 5px;
  margin-top: 5px;
  border-radius: 6px;
  color: white;
  text-align: center;
  display: flex;  
  justify-content: center;
  align-items: center;
  opacity: 0;
}

.error {
  background-color: #FF5733;
}

.success {
  background-color: #0BDA51;
}

.notification.show {
  opacity: 1;
  -webkit-animation: fadein 0.5s, fadeout .5s 1.5s;
  animation: fadein 0.5s, fadeout .5s 1.5s;
}

@keyframes fadein {
  from {top: 0; opacity: 0;}
  to {top: 5%; opacity: 1; }
}

@keyframes fadeout {
  from {top: 5%; opacity: 1;}
  to {top: 0; opacity: 0;}
}
Enter fullscreen mode Exit fullscreen mode

Now that we have the looks, we need the functionality. So let’s add some JavaScript too:

script.js

window.addEventListener('DOMContentLoaded', () => {
  const ulElement = document.querySelector('.grocery-list');
  const submitElement = document.querySelector('.submit');
  const inputElement = document.querySelector('#grocery-input');
  const errorElement = document.querySelector('.error');
  const successElement = document.querySelector('.success');
  let dateSelected = null;

  const showNotificationMessage = (element, errorMessage) => {
    if (element.classList.contains('show')) {
      return;
    }
    element.textContent = errorMessage;
    element.classList.add('show');

    setTimeout(() => {
      element.classList.remove('show');
    }, 2000)
  }

  const datePicker = new easepick.create({
    element: '#datepicker',
    css: [
      "https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.0/dist/index.css"
    ],

    zIndex: 10,
    setup(picker) {
      picker.on('select', (e) => {
        dateSelected = e.detail.date;
      })
    }
  });

  ulElement.addEventListener('click', (e) => {
    if (e.target.tagName === 'I') {
      ulElement.removeChild(e.target.closest('li'));
    }
  })

  inputElement.addEventListener('keyup', (e) => {
    const value = e.target.value;

    if (e.keyCode === 13 && value.trim()) {
      const li = document.createElement('li');
      li.classList.add('grocery-list-item');

      const span = document.createElement('span');
      span.classList.add('grocery-item');
      span.textContent = value;

      const icon = document.createElement('i');
      icon.classList.add('uil', 'uil-trash', 'delete-icon');

      li.appendChild(span);
      li.appendChild(icon);

      ulElement.appendChild(li);

      inputElement.value = '';
    }
  });

  submitElement.addEventListener('click', (e) => {
    const groceryItems = [...document.querySelectorAll('span.grocery-item')].map(element => ({
      item: element.textContent
    }));

    if (!dateSelected) {
      return showNotificationMessage(errorElement, 'Please select the grocery date.');
    }

    const date2DaysBefore = new Date(dateSelected.setDate(dateSelected.getDate() - 1));

    if (new Date() > date2DaysBefore) {
      return showNotificationMessage(errorElement, 'Please select a date two days or more after this day.');
    }

    if (!groceryItems.length) {
      return showNotificationMessage(errorElement, 'Please add grocery items.');
    }

    fetch('http://localhost:3000/grocery-schedule', {
      method: 'POST',
      body: JSON.stringify({
        scheduledGroceryDate: dateSelected.toISOString(),
        groceryItems
      }),
      headers: {
        'content-type': 'application/json'
      },
      mode: 'cors'
    })
      .then(resp => resp.json())
      .then((resp) => {

        while (ulElement.lastChild) {
          ulElement.removeChild(ulElement.lastChild);
        }
        showNotificationMessage(successElement, resp.message);
      })
      .catch(e => console.log(e))
  })
})
Enter fullscreen mode Exit fullscreen mode

In our script.js file, we:

  • Initialize our easepick instance, and in the setup method, we are listening to the select event so we can get the date value.
  • For ulElement we’re listening for the click event, and in the callback, we’re checking for the I element because that is the delete button for our unordered list.
  • In inputElement we’re listening for keyup event so that we can add a new item in the unordered list element.
  • And lastly, submitElement is listening for the click event for this element and will send the request to our API will trigger scheduling.

We will be triggering the email the day before the scheduled date to remind us earlier.

Image description

This is what the web app looks like in the front end with one grocery item.

Creating Our API

Now we’ll be needing to create our backend with Node and Express. Specifically, we need Node version 18.12.0. There’s a command line utility that we can use to switch to the version we need.

mkdir api && cd api && npm init -y && npm install express cors @novu/node
Enter fullscreen mode Exit fullscreen mode

What we did here is that we created the API folder and after that, initialize our node project with the npm init -y command using the default configuration. After that, we installed the libraries that we will be using. We’re not saving our data in the database, but we’ll get to that later. We also installed the Novu @novu/node package so that we can notify from our API.

package.json

{
  "name": "grocery-notify-app",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "start": "node app.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@novu/node": "^0.11.0",
    "cors": "^2.8.5",
    "express": "^4.18.2"
  },
  "type": "module"
}
Enter fullscreen mode Exit fullscreen mode

config.js

const API_KEY = 'API_KEY'; // Novu Dashboard -> Settings -> Api Keys Tab
const SUBSCRIBER_ID = 'SUBSCRIBER_ID'; // subscriber id created by the sdk or from the workflow
const EMAIL = 'TO_EMAIL'; // your EMAIL to receive the notification
const PORT = 3000;

export {
  API_KEY,
  EMAIL,
  SUBSCRIBER_ID,
  PORT
}
Enter fullscreen mode Exit fullscreen mode

We will be getting API_KEY and the SUBSCRIBER_ID from the Novu Dashboard later.

app.js

import express from "express";
import cors from "cors";
import { API_KEY, SUBSCRIBER_ID, EMAIL, PORT } from "./config.js";
import { Novu } from "@novu/node";

const novu = new Novu(API_KEY);
const app = express();

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

app.post('/grocery-schedule', async (req, res) => {
  const { scheduledGroceryDate, groceryItems } = req.body;

  try {
    res.status(200).send({ message: "Grocery Reminder Scheduled." })
  } catch (e) {
    console.log(e);
    res.status(500).send({
      message: 'Something went wrong, when scheduling the grocery reminder.'
    })
  }
});

app.listen(PORT, () => {
  console.log(`server listening at port ${PORT}`);
})
Enter fullscreen mode Exit fullscreen mode

In our app.js we have one endpoint: /grocery-schedule. This is where we will handle the notification that we receive in our email. Using Novu is really easy, getting the Novu constructor from the module and creating a new Novu instance. Grab your API_KEY from your Novu account dashboard.

What is Novu?

Basically, Novu is a platform for implementing, configuring and managing notifications in our application. It can manage:

  • Email
  • SMS
  • Chat
  • Push Notifications

All on one platform! Novu makes it easier when building real-time applications, all we need to do is to configure and trigger the notification in our code. You can read more about Novu here. You can easily create an account with Novu with your GitHub account, which is probably the fastest for this tutorial.

When you first see the Novu dashboard, you will be seeing the dashboard.

Image description

As I said earlier, we can get the API key here in the dashboard, specifically in Settings → Api Keys.

Image description

After that, we need to make a notification template that we will trigger in our API. Click **Notifications* and click the New button on the far right.

Image description

Put the necessary details about the notification template like the Notification Name, Notification Identifier, and Notification Description. Before we create the workflow, let’s take a look at the Integrations Store tab.

Image description

As I said earlier, Novu has a list of notification integration options that you can work with like email, sms, chat and push notifications. This is where you integrate the Notification Template that we made earlier into a specific provider. We’ll use Mailjet as the email provider to send the grocery reminder to a specific email.

First, your email must have a valid domain to make this work. I’ll be using my work email for this.

Image description

You can get your Mailjet API key and secret key in your Account SettingsRest API → API Key Management (Primary and Sub-account).

Image description

Copy the API key and secret key and go to Novu DashboardIntegrations* Store → Mailjet Provider and paste it. Make sure to include the email that you used for Mailjet.

Image description

After this, we need to edit our workflow editor for our notification. In your Novu dashboard go to Notifications and click the notification that you created earlier. For me it’s grocery-notification.

Image description

Once we’ve done that, click the Workflow Editor tab and in that in the editor click the circle with the plus sign below the Trigger component and select Delay on the right side.

There are two types of Delay, Regular and Scheduled. For this, we’ll use the Scheduled type. We also need to specify the name of the field that Novu will be using for email. In my example, I will be using the sendAt field.

Image description

And below the Delay component click the circle with the plus sign again and select Email.

Image description

Our workflow should look like this:

Image description

Lastly, we also need to configure our email template. Click the Edit Template button on the right below Email Properties. After the redirection to the Edit Email Template UI, the email you’re seeing here is the email that you used in the Mailjet configuration earlier. Update the email subject to Grocery Reminder and click the Custom Code and copy this Handlebars code below:

<div>
  <h1>Hi, It's time for you to buy your grocery items.</h1>
  <h2>{{dateFormat date 'MM/dd/yyyy'}}</h2>
  {{#each groceryItems}} 
    <li>{{item}}</li>
  {{/each}}
</div>

Enter fullscreen mode Exit fullscreen mode

This code is pretty simple. In the h2 we’re formatting the date value to Month/Day/Year format, and in the part where we use the #each we’re just iterating in our groceryItems array. We’re also using the item property that we get from each item iteration in the list item element, and we need to specify the /each after the iteration.

Image description

After that, click the Update button on the top right side.

Finishing Our API

With all of that out of the way, it’s time to complete the API of our app. Update the config.js file with the API key you got earlier from the Novu Dashboard, the SUBSCRIBER_ID, and email from Notifications in the dashboard. The EMAIL is optional here to use since the SUBSCRIBER_ID will use that same value, but for example purposes, we will provide it.

Image description

And our updated config.js file:

const API_KEY = 'YOUR_API_KEY_FROM_THE_SETTINGS_TAB'; // Novu Dashboard -> Settings -> Api Keys Tab
const SUBSCRIBER_ID = '63dd93575fd0df47313ee933';
const EMAIL = 'mac21macky@gmail.com';
const PORT = 3000;

export {
  API_KEY,
  EMAIL,
  SUBSCRIBER_ID,
  PORT
}
Enter fullscreen mode Exit fullscreen mode

Lastly, we need to update our app.js file to trigger Novu for scheduling our notification:

import express from "express";
import cors from "cors";
import { API_KEY, SUBSCRIBER_ID, EMAIL, PORT } from "./config.js";
import { Novu } from "@novu/node";

const novu = new Novu(API_KEY);
const app = express();

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

app.post('/grocery-schedule', async (req, res) => {
  const { scheduledGroceryDate, groceryItems } = req.body;
  try {
    // sendAt - 9 am on the `scheduledGroceryDate`
    const sendAt = new Date(new Date(scheduledGroceryDate).setHours(9, 0, 0, 0)).toISOString();

    await novu.trigger('grocery-notification', {
      to: {
        subscriberId: SUBSCRIBER_ID,
        email: EMAIL
      },
      payload: {
        sendAt,
        date: new Date(scheduledGroceryDate).toISOString(),
        groceryItems
      }
    });
    res.status(200).send({ message: "Grocery Reminder Scheduled." })
  } catch (e) {
    console.log(e);
    res.status(500).send({
      message: 'Something went wrong, when scheduling the grocery reminder.'
    })
  }
});

app.listen(PORT, () => {
  console.log(`server listening at port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

Basically, what we’re doing here is creating the scheduled date value sendAt. This will have the value of 9 am of scheduledGroceryDate. For example, if we provide the value of March 12, 2023, then the sendAt value will be March 12, 2023 9:00 am. The trigger method from the novu instance from the name itself will trigger our notification. It accepts the Trigger ID as the first parameter, which you can get from Novu.

Image description

In the to object we provide the SUBSCRIBER_ID and EMAIL from our config.js and in the payload object we provide sendAt, date and groceryItems. Remember that sendAt and date fields must be in ISO string format.

Okay! Let’s test our application. I’m gonna add 5 new items to our grocery list: Milk, Bread, Water, Butter, and Grapes.

Image description

Press the Schedule button, and if went successfully, it will pop up a message that tells us it’s been scheduled.

Image description

Going back to Novu, in the Activity Feed, you can see if that particular reminder is scheduled.

Image description

And when you click the first one you can know the details of that notification.

Image description

The execution for this notification is delayed because we specified it as a Delay type earlier and you can also see the time when this notification will be executed.

This is a sample email that you will get if it all goes as planned.

Image description

If you want to expand this project, you can add a database to our project for saving grocery items. Then, show the list of grocery dates that are scheduled on another page in our web app.

Closing the Shopping List

So far, we’ve created an app that uses some simple Node.js and Express to make a simple grocery list. Then, we use the power of Novu to schedule notifications and send emails to remind ourselves. On the surface, it’s pretty simple, but a lot of room for added functionality. You can set up user accounts and share it with your family, or maybe add SMS notifications too!

Novu is a great open source notification system that you can use for notifications to integrate with your applications. It’s free to get started, and a very powerful tool in the right hands.

If you’d like to see the full project Github, you can check it out here.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .