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
, anduseState
are imported from the 'react' package.Client
,Databases
,ID
, andQuery
are imported from the 'appwrite' package - An instance of the
Client
class from theappwrite
package is created and configured with the endpoint and project ID - An instance of the
Databases
class is created using theClient
instance, which provides access to the database-related functionalities - Within the
HomePage
component, thepoints
state is initialized using theuseState
hook, with an initial value of 0 - An array called
stores
is 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:
-
React
is imported from the 'react' package and theLink
component is imported from ‘next/link’ - The
Store
component is defined as a functional component which receivesname
,discount
,store
, andpoints
as props - The JSX markup is returned, representing the structure and UI of a store card and includes elements styled with
Pink Design
- The
Link
component from Next.js is used to create a clickable link for purchasing from the store - The
href
prop of theLink
component is set to a dynamically generated URL. Thestore
parameter 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 theuseRouter
hook from Next.js are imported from their respective packages;Client
,Databases
,ID
, andQuery
are imported from the 'appwrite' package, andLink
is imported from Next.js - An instance of the
Client
class from theappwrite
package is created and configured with the endpoint and project ID - An instance of the
Databases
class is created using theClient
instance, which provides access to the database-related functionalities - The
useRouter
hook is used to access the query parameters from the URL. Thestore
andpoints
values are extracted from therouter.query
object - Within the component, the
rewardPoints
state is initialized using theuseState
hook with an initial value of 0 (this state will hold the earned reward points) - The
purchaseComplete
state is also initialized using theuseState
hook with an initial value offalse
and its state will track whether the purchase has been completed - 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 - A
product
object is defined with propertiesname
andprice
- 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
useEffect
hook calls thecheckUser
function when the component mounts; since an empty dependency array is provided as the second argument, it only runs once - The
storePoints
function is an asynchronous function that creates a document in the database using thecreateDocument
method from theDatabases
instance; it stores the unique ID of the user and the points they have - The
checkUser
function is an asynchronous function that fetches the user's IP address using thefetch
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, thestorePoints
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 thesetPoints
function
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.