How to build a cloud-based loyalty rewards app in Next.js
Tags: webdev, javascript, tutorial, appwrite
Published: 2023-07-13 09:26:15

Loyalty rewards programs are crucial in fostering customer engagement and driving customer retention. Loyalty rewards programs incentivize customers to keep coming back and promote a sense of engagement and satisfaction. 

In this article, we will build a powerful loyalty rewards app using Next.js, a popular React framework, alongside the robust Appwrite Cloud backend-as-a-service platform and the visually stunning [Pink Design Library](https://pink.appwrite.io/?utm_source=hackmamba&utm_medium=hackmamba-blog). 

This app will have a list of stores and the user will gain points by buying from these stores and these points would be added to the user’s account.

## Prerequisites

To follow along with this article you will need:

1. A basic understanding of JavaScript and Next.js
2. An Appwrite Cloud account (you can create one [here](https://cloud.appwrite.io/register))

## Repository

Find the complete code used in this article on [GitHub](https://github.com/folucode/loyalty-rewards-app).

## Project setup

[Node](https://nodejs.org/en/) needs to be installed on our computer to set up the Next.js application. To install Node, go to the [Node.js](https://nodejs.org/en/) [website](https://nodejs.org/en/) and follow the instructions to install the specific software that is compatible with our operating system. 

We can verify the Node.js installation by running the command below:


    node -v
    v18.15.0 //node version installed

To create the Next.js app, run the command below. It will automatically set up a boilerplate Next.js app.

`npx` stands for Node Package eXecute. It executes any package from the npm registry without installing it.


    npx create-next-app@latest <app-name>
    # or
    yarn create next-app <app-name>

After the installation is complete, change the directory into the app we just created:


    cd <app-name>

Next, install these dependencies:


    npm i @appwrite.io/pink appwrite

Run `npm run dev` or `yarn dev` to start the development server on `http://localhost:3000`.

## Setting up the Appwrite database

We’ll be using [Appwrite Cloud](https://cloud.appwrite.io/?utm_source=hackmamba&utm_medium=hackmamba-blog) database service in this app.

To set up an Appwrite Cloud database, log in to the Appwrite console and create a new project. 

![Appwrite personal projects dashboard](https://paper-attachments.dropboxusercontent.com/s_C6099BC2312208D0DE1CA93CAC707D00F400AC90E68296F42F0B1B2287C6E09B_1685996915225_1.png)


Next, click on **`Databases`** and then **`Create database`**. We can name it **`rewards-app`**.

![Appwrite databases dashboard](https://paper-attachments.dropboxusercontent.com/s_C6099BC2312208D0DE1CA93CAC707D00F400AC90E68296F42F0B1B2287C6E09B_1685996929232_2.png)


In the database, create a collection named **users** and in the collection, add the attributes below with their respective types:
[](http://asd)
1. `points` - `integer`
2. `userID` - `integer`

[](http://asd)
![Appwrite databases collection dashboard](https://paper-attachments.dropboxusercontent.com/s_C6099BC2312208D0DE1CA93CAC707D00F400AC90E68296F42F0B1B2287C6E09B_1685997400302_3.png)

[](http://asd)
Click the `Indexes` tab and add an index on the `userID` attribute. Next, go to the settings tab and then scroll down to `Update permissions`. Add a new role for `Any` and then give the role all permissions.


![Appwrite permissions dashboard](https://paper-attachments.dropboxusercontent.com/s_C6099BC2312208D0DE1CA93CAC707D00F400AC90E68296F42F0B1B2287C6E09B_1685997685857_4.png)

## Setting up the app

Inside the **`pages`** directory, create the following files:

1. **`store.js`**: From this page we will list the fictional grid of stores where points can be redeemed
2. **`purchase.js`**: This page will simulate the completion of a purchase and issue a reward to the user

## Setting up Pink Design

Open the `_app.js` file and replace the existing code with the following:

```javascript
    import '@appwrite.io/pink';
    import '@appwrite.io/pink-icons';
    
    function MyApp({ Component, pageProps }) {
      return <Component {...pageProps} />;
    }
    
    export default MyApp;
 ```
   
### Setting up the components

Open **`pages/index.js`** and replace the existing code with the following:

```javascript
    import React, { useEffect, useState } from 'react';
    import { Client, Databases, ID, Query } from 'appwrite';
    
    const client = new Client()
      .setEndpoint('https://cloud.appwrite.io/v1')
      .setProject('[PROJECT-ID]');
    
    const databases = new Databases(client);
    
    const HomePage = () => {
      const [points, setPoints] = useState(0);
      const stores = [
        { name: 'Store 1', discount: '10% off', points: 10 },
        { name: 'Store 2', discount: '20% off', points: 20 },
        { name: 'Store 3', discount: '30% off', points: 30 },
        { name: 'Store 4', discount: '40% off', points: 40 },
      ];
    
      return (
        <div
          className='container'
          style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}
        >
          <h1
            style={{
              fontSize: '32px',
              marginBottom: '20px',
              color: 'hsl(var(--color-neutral-300))',
            }}
          >
            Rewards App
          </h1>
          <div
            style={{
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'space-between',
              marginBottom: '20px',
            }}
          >
            <p style={{ fontSize: '24px', color: 'hsl(var(--color-neutral-300))' }}>
              Total Points:
            </p>
            <p style={{ fontSize: '32px', fontWeight: 'bold', color: '#0070f3' }}>
              {points}
            </p>
          </div>
          <div
            style={{
              gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
              gridGap: '20px',
            }}
            className='grid-box'
          >
            {stores.map((store, index) => (
              
            ))}
          </div>
        </div>
      );
    };
    
    export default HomePage;
```    

Let's go through the code and explain what is happening:


1. **`React`**, **`useEffect`**, and **`useState`** are imported from the 'react' package. **`Client`**, **`Databases`**, **`ID`**, and **`Query`** are imported from the 'appwrite' package
2. An instance of the **`Client`** class from the **`appwrite`** package is created and configured with the endpoint and project ID
3. An instance of the **`Databases`** class is created using the **`Client`** instance, which provides access to the database-related functionalities
4. Within the **`HomePage`** component, the **`points`** state is initialized using the **`useState`** hook, with an initial value of 0
5. An array called **`stores`** is declared, which contains objects representing different stores, each having properties like name, discount, and points
6. The JSX markup representing the structure and UI of the homepage is styled with `Pink Design`'s utility classes

Open **`pages/stores.js`** and add the following code:

```javascript
    import React from 'react';
    import Link from 'next/link';
    
    const Store = ({ name, discount, store, points }) => {
      return (
        <div
          style={{
            border: '1px solid #ddd',
            textAlign: 'center',
            backgroundColor: '#f9f9f9',
          }}
          className='u-padding-24'
        >
          <h3 style={{ fontSize: '18px', marginBottom: '10px' }}>{name}</h3>
          <p style={{ fontSize: '16px', color: '#0070f3' }}>{discount}</p>
          <Link
            style={{
              marginTop: '20px',
              textAlign: 'center',
              textDecoration: 'none',
              backgroundColor: '#0070f3',
              fontWeight: 'bold',
              fontSize: '16px',
              color: '#fff',
              border: 'none',
              padding: '10px 20px',
              cursor: 'pointer',
              transition: 'backgroundColor 0.3s ease',
            }}
            className='u-block'
            href={`/purchase?store=${encodeURIComponent(store)}&points=${points}`}
          >
            <span>Purchase</span>
          </Link>
        </div>
      );
    };
    
    export default Store;
```    

Let's go through the code and explain what is happening:


1. `React` is imported from the 'react' package and the **`Link`** component is imported from ‘next/link’
2. The **`Store`** component is defined as a functional component which receives **`name`**, **`discount`**, **`store`**, and **`points`** as props
3. The JSX markup is returned, representing the structure and UI of a store card and includes elements styled with `Pink Design`
4. The **`Link`** component from Next.js is used to create a clickable link for purchasing from the store
5. The **`href`** prop of the **`Link`** component is set to a dynamically generated URL. The **`store`** parameter is URI encoded using **`encodeURIComponent()`**

Open **`pages/purchase.js`** and add the following code:

```javascript
    import React, { useState } from 'react';
    import { useRouter } from 'next/router';
    import { Client, Databases, ID, Query } from 'appwrite';
    import Link from 'next/link';
    
    const client = new Client()
      .setEndpoint('https://cloud.appwrite.io/v1') // Our API Endpoint
      .setProject('[PROJECT-ID]');
    const databases = new Databases(client);
    
    const PurchasePage = () => {
      const router = useRouter();
      const { store, points } = router.query;
      const [rewardPoints, setRewardPoints] = useState(0);
      const [purchaseComplete, setPurchaseComplete] = useState(false);
    
      const buttonStyle = {
        backgroundColor: purchaseComplete ? '#ccc' : '#0070f3',
        color: '#fff',
        border: 'none',
        padding: '10px 20px',
        fontSize: '16px',
        cursor: purchaseComplete ? 'not-allowed' : 'pointer',
        transition: 'background-color 0.3s ease',
      };
    
      const product = { name: 'Product 1', price: 50 };
    
      const handlePurchase = async (price) => {
        const reward = Math.floor(price * (points / 100));
        const documentInfo = JSON.parse(localStorage.getItem('documentInfo'));
        const collectionId = documentInfo.info.$collectionId;
        const documentId = documentInfo.info.$id;
        const databaseId = documentInfo.info.$databaseId;
        const currentPoints = documentInfo.info.points;
    
        await databases.updateDocument(databaseId, collectionId, documentId, {
          points: currentPoints + reward,
        });
    
        setRewardPoints(reward);
        setPurchaseComplete(true);
      };
    
      return (
        <div
          className='container u-padding-24'
          style={{ maxWidth: '800px', margin: '0 auto' }}
        >
          <h1
            style={{
              fontSize: '32px',
              marginBottom: '20px',
              color: 'hsl(var(--color-neutral-300))',
              textAlign: 'center',
            }}
          >
            Complete Purchase and Earn Rewards
          </h1>
          <div
            style={{
              gridTemplateColumns: 'repeat(1, 1fr)',
              gridGap: '20px',
            }}
            className='grid-box'
          >
            <div
              style={{
                border: '1px solid #ddd',
                textAlign: 'center',
                backgroundColor: '#f9f9f9',
                alignSelf: 'center',
                width: '300px',
                justifySelf: 'center',
              }}
              className='u-padding-24'
            >
              <h3>{product.name}</h3>
              <p>Price: ${product.price}</p>
              <button
                style={buttonStyle}
                className='u-margin-32'
                onClick={() => handlePurchase(product.price)}
                disabled={purchaseComplete}
              >
                {purchaseComplete ? 'Purchased' : 'Purchase'}
              </button>
            </div>
          </div>
          {purchaseComplete && (
            <p
              style={{
                marginTop: '20px',
                fontSize: '16px',
                color: 'green',
                textAlign: 'center',
              }}
            >
              Congratulations! You earned {rewardPoints} reward points for your
              purchase at {store}.{' '}
              <Link style={{ color: 'blue', textDecoration: 'underline' }} href='/'>
                Go Home
              </Link>
            </p>
          )}
        </div>
      );
    };
    
    export default PurchasePage;
```    

Let's go through the code and explain what is happening:


1. **`React`**, **`useState`**, and the **`useRouter`** hook from Next.js are imported from their respective packages; **`Client`**, **`Databases`**, **`ID`**, and **`Query`** are imported from the 'appwrite' package, and **`Link`** is imported from Next.js
2. An instance of the **`Client`** class from the **`appwrite`** package is created and configured with the endpoint and project ID
3. An instance of the **`Databases`** class is created using the **`Client`** instance, which provides access to the database-related functionalities
4. The **`useRouter`** hook is used to access the query parameters from the URL. The **`store`** and **`points`** values are extracted from the **`router.query`** object
5. Within the component, the **`rewardPoints`** state is initialized using the **`useState`** hook with an initial value of 0 (this state will hold the earned reward points)
6. The **`purchaseComplete`** state is also initialized using the **`useState`** hook with an initial value of **`false`** and its state will track whether the purchase has been completed
7. The **`buttonStyle`** object is defined with inline styles for the purchase button; it has a function set in the onClick handler, **`handlePurchase`**, which we will create later in the article
8. A **`product`** object is defined with properties **`name`** and **`price`**
9. The JSX markup representing the structure and UI of the purchase page is styled with `Pink Design`

## Building the functionality

In the `index.js` file, we will create two functions:


1. One is to check that the current user exists in the database, and if so, we will display their points. Since we aren't building an authentication system, we would use the user's IP address to ensure it's unique.
2. The other function is to store the user's points in the database.

Modify the `index.js` file like so:

```javascript
    ...
    const HomePage = () => {
      ...
    
      useEffect(() => {
        checkUser();
      }, []);
    
      const storePoints = async (uniqueID, points) => {
        await databases.createDocument(
          '646a20a583e20fd44d35',
          '646a2112a4601b39a496',
          ID.unique(),
          { userID: uniqueID, points }
        );
      };
    
      const checkUser = async () => {
        let userIP = await fetch('https://api.ipify.org?format=json')
          .then((response) => response.json())
          .then(async (data) => data.ip)
          .catch((error) => {
            console.error('Error fetching IP address:', error);
          });
    
        const user = await databases.listDocuments(
          '[DATABASE-ID]',
          '[COLLECTION-ID]',
          [Query.equal('userID', userIP)]
        );
    
        if (user.total < 1) {
          storePoints(userIP, 0);
        } else {
          localStorage.setItem(
            'documentInfo',
            JSON.stringify({ info: user.documents[0] })
          );
          setPoints(user.documents[0].points);
        }
      };
    
      return (
    ...
```

1. The **`useEffect`** hook calls the **`checkUser`** function when the component mounts; since an empty dependency array is provided as the second argument, it only runs once
2. The **`storePoints`** function is an asynchronous function that creates a document in the database using the **`createDocument`** method from the **`Databases`** instance; it stores the unique ID of the user and the points they have
3. The **`checkUser`** function is an asynchronous function that fetches the user's IP address using the **`fetch`** API, it then checks if the user exists in the database by querying the database with the user's IP address. If the user doesn't exist, the **`storePoints`** function creates a new document with the user's IP and 0 points. If the user does exist, the document information is stored in the local storage and the points are set using the **`setPoints`** function

Next, display the list of fictional stores by adding the Store component to the mapping of the **`stores`** array:

```javascript
    ...
    return (
        ...
            {stores.map((store, index) => (
              <Store
                key={index}
                name={store.name}
                discount={store.discount}
                store={store.name}
                points={store.points}
              />
            ))}
    ...
 ```   
## Handling purchases

Open the `purchase.js` file and add the following function:

```javascript
    ...
    
    const PurchasePage = () => {
    
    ...
      const handlePurchase = async (price) => {
        const reward = Math.floor(price * (points / 100));
        const documentInfo = JSON.parse(localStorage.getItem('documentInfo'));
        const collectionId = documentInfo.info.$collectionId;
        const documentId = documentInfo.info.$id;
        const databaseId = documentInfo.info.$databaseId;
        const currentPoints = documentInfo.info.points;
    
        await databases.updateDocument(databaseId, collectionId, documentId, {
          points: currentPoints + reward,
        });
    
        setRewardPoints(reward);
        setPurchaseComplete(true);
      };
    
      return (
        ...
```    

The **`handlePurchase`** function defined above is called when the purchase button is clicked, it calculates the reward points based on the price and the points query parameter. It then retrieves the necessary information from the **`localStorage`** and updates the user's points in the database using the **`databases.updateDocument`** method. The **`rewardPoints`** state is updated with the calculated reward points, and the **`purchaseComplete`** state is set to **`true`** to blur the purchase button.

## The final result
https://www.loom.com/share/87e5b4efd14e4e95aad24e49c02ebec7

## Conclusion

In conclusion, we built a cloud-based loyalty rewards app in Next.js by integrating Appwrite Cloud and Pink Design. Appwrite Cloud allowed us to seamlessly manage and efficiently track user points with easy database interaction. Pink Design added a touch of elegance and user-friendliness to the app's interface, enhancing the overall visual appeal and usability.

We can extend the app's functionality by integrating authentication and a checkout payment system. The app can provide personalized experiences and secure user registration and login processes by implementing user authentication. Furthermore, integrating a checkout payment system enables users to redeem their earned loyalty points for products or services.

## Resources
1. [NextJS documentation](https://nextjs.org/docs)
2. [Appwrite Cloud](https://appwrite.io/docs)
3. [Pink Design](https://pink.appwrite.io/)