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.
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:
- A basic understanding of JavaScript and Next.js
- An Appwrite Cloud account (you can create one here)
Repository
Find the complete code used in this article on GitHub.
Project setup
Node needs to be installed on our computer to set up the Next.js application. To install Node, go to the Node.js website 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 database service in this app.
To set up an Appwrite Cloud database, log in to the Appwrite console and create a new project.
Next, click on Databases and then Create database. We can name it rewards-app.
In the database, create a collection named users and in the collection, add the attributes below with their respective types:
-
points-integer -
userID-integer
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.
Setting up the app
Inside the pages directory, create the following files:
-
store.js: From this page we will list the fictional grid of stores where points can be redeemed -
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:
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:
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:
-
React,useEffect, anduseStateare imported from the 'react' package.Client,Databases,ID, andQueryare imported from the 'appwrite' package - An instance of the
Clientclass from theappwritepackage is created and configured with the endpoint and project ID - An instance of the
Databasesclass is created using theClientinstance, which provides access to the database-related functionalities - Within the
HomePagecomponent, thepointsstate is initialized using theuseStatehook, with an initial value of 0 - An array called
storesis declared, which contains objects representing different stores, each having properties like name, discount, and points - 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:
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:
-
Reactis imported from the 'react' package and theLinkcomponent is imported from ‘next/link’ - The
Storecomponent is defined as a functional component which receivesname,discount,store, andpointsas props - The JSX markup is returned, representing the structure and UI of a store card and includes elements styled with
Pink Design - The
Linkcomponent from Next.js is used to create a clickable link for purchasing from the store - The
hrefprop of theLinkcomponent is set to a dynamically generated URL. Thestoreparameter is URI encoded usingencodeURIComponent()
Open pages/purchase.js and add the following code:
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:
-
React,useState, and theuseRouterhook from Next.js are imported from their respective packages;Client,Databases,ID, andQueryare imported from the 'appwrite' package, andLinkis imported from Next.js - An instance of the
Clientclass from theappwritepackage is created and configured with the endpoint and project ID - An instance of the
Databasesclass is created using theClientinstance, which provides access to the database-related functionalities - The
useRouterhook is used to access the query parameters from the URL. Thestoreandpointsvalues are extracted from therouter.queryobject - Within the component, the
rewardPointsstate is initialized using theuseStatehook with an initial value of 0 (this state will hold the earned reward points) - The
purchaseCompletestate is also initialized using theuseStatehook with an initial value offalseand its state will track whether the purchase has been completed - The
buttonStyleobject 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 - A
productobject is defined with propertiesnameandprice - 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:
- 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.
- The other function is to store the user's points in the database.
Modify the index.js file like so:
...
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 (
...
- The
useEffecthook calls thecheckUserfunction when the component mounts; since an empty dependency array is provided as the second argument, it only runs once - The
storePointsfunction is an asynchronous function that creates a document in the database using thecreateDocumentmethod from theDatabasesinstance; it stores the unique ID of the user and the points they have - The
checkUserfunction is an asynchronous function that fetches the user's IP address using thefetchAPI, 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, thestorePointsfunction 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 thesetPointsfunction
Next, display the list of fictional stores by adding the Store component to the mapping of the stores array:
...
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:
...
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.




Top comments (0)