Notification system for a grocery app with React and NodeJS
Published: 2023-03-15 12:23:10

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gdys7bptc0sijr99iejr.gif)

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](https://github.com/MacMacky/grocery-notification).*

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`

```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>
```

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](https://easepick.com/). It’s a pretty simple library to drop into our app, and we’ll use it alongside [icons from Unicons](https://iconscout.com/unicons).

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

`styles.css`

```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;}
}
```

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

`script.js`

```jsx
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))
  })
})
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qiei6mbx0f244lm683sj.png)

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](https://github.com/nvm-sh/nvm) that we can use to switch to the version we need.

```bash
mkdir api && cd api && npm init -y && npm install express cors @novu/node
```

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`

```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"
}
```

`config.js`

```jsx
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
}
```

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

`app.js`

```jsx
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}`);
})
```

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](https://web.novu.co/auth/login).

## 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](https://novu.co/). 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eau19rkxhx01118izved.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o00aiwfww3nlozp69601.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wootqwosbkr2yq63692y.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1rofexzamgwursulw2nr.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/akipgyse37i2flzem2ce.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m4voawpoabh696uc9p8k.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/94w5m9yxp3kcn9sdapap.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3021vyw7gre8eozdx2ff.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ywfi5attucgzk6kdrrx2.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b07gle8zepejr6p2myd2.png)

Our workflow should look like this:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bu6v96av14zpsjopuua7.png)

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:

```jsx
<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>

```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p1egkgkcnzur5v1w94n6.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k6m4hbvqh7eneaatuvl1.png)

And our updated `config.js` file:

```jsx
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
}
```

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

```jsx
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}`);
});
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f4iqup45b0ztiubyhvav.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h6irgmad2c478bshjfee.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5kc9t4tqcw74iarzszss.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f0aood40c9q1xiphkc5l.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d70pjw07ebw0mnh8d7d5.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lcbhn087fgvr6o2xbd9b.png)

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](https://novu.co/), 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](https://github.com/MacMacky/grocery-notification).*