Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Frontend demo showcaing use of Redpanda Serverless with Vercel & Push… #29

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions frontend-vercel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Overview

This real-time, interactive application is designed to keep fans informed about the current stock of cupcakes for a new cupcake business in New York City. Leveraging the power of Redpanda Serverless for data streaming and Vercel for deployment, the app offers instantaneous updates on cupcake availability, enhancing customer engagement and inventory management.

## Features

- __Real-time Cupcake Stock Updates__: Utilizes Redpanda Serverless to stream data about cupcake availability.
- __Interactive Map__: Leverages LeafletJS for displaying dynamic updates of cupcake stocks across different locations.
- __WebSocket Updates__: Incorporates Pusher Channel/Websocket for real-time, serverless data push to the app.
- __Simplified Deployment__: Hosted on Vercel or you can run locally for easy deployment.

![App Screenshot](img/theapp.png)

## Technology Stack

- **Redpanda Serverless**: For creating and managing data streams with ease and efficiency.
- **NodeJs**: The backend framework used for server-side logic.
- **LeafletJS**: A JavaScript library for mobile-friendly interactive maps.
- **Vercel**: For hosting the NodeJS application and providing seamless deployment and GitHub integration.
- **Pusher Channel**: For WebSocket service that enables real-time data push to the application.

## Applicaticon Structure and Files

![App structure Screenshot](img/cupcake-explained.png)

- `index.js`: Serves as the main server file, setting up the Express server, defining routes
- submitting inventory updates _/submit_ and interacts with Kafka to produce messages whenever inventory data is submitted
- clearing inventory _/clean-inventory_
- serving the main app _/_
- input form _/input_
- `mapview.js`: Handles the consuming from Redpanda, updating the inventory list(memorymap ), and then using Pusher to push these updates to the front end (optionally go to `cupcake` folder to push to websocket service ). It also locads the store IDs to their locations, ensuring that inventory updates can be accurately represented on a map.
- `store_nyc.csv`: List of cupcake partner stores with geographical locations and names in NYC
- `index.html`: Front end of the application, displaying the real-time map and inventory updates to users.
- `input.html & input.js`: Form for submitting cupcake inventory updates.
- `submit.js`: Handle the submission of inventory updates from the client side, posting data to the server.


## Getting Started

### Setup Redpanda Serverless

1. Sign up and navigate to the Redpanda Web UI.
2. Click on the "Create Cluster" button and select a region close to your workload.
3. Create a user by going to *Security* on the left menu, and set permissions (at least enable read and write to topic name `inv-count`)
4. Create a topic called `inv-count` for cupcake stock updates
![Redpanda Serverless steps](img/serverless.png)

### Initial Setup
You can choose either the `cupcake` or `cupcake-pusher` directory based on your real-time update preference and navigate into the chosen folder.
- `cupcake`: allows you to run the application locally in your machine without the need from Pusher.
- `cupcake-pusher`: lets you deploy the application on Vercel and use *Pusher* to push update to the web page



### Option one: Running locally
1. Clone the repository and navigate to the project directory. `git clone https://github.com/weimeilin79/cupcakefanatic.git`
2. Go to folder `cupcake`
3. Set your local environment variable from Redpanda Serverless
```
export SERVERLESSBROKER=<YOUR_SERVERLESS_BOOSTRAP_URL>
export RPUSER=<USER_YOU_CREATED_IN_RP_SERVERLESS>
export RPPWD=<USER_YOU_CREATED_IN_RP_PWD>
```
4. Install the NodeJS application dependencies by running `npm install`
5. Start the application `node index.js`
6. Your application will be ready on in `http://localhost:3000`


### Option two: Deploying to Vercel
1. Fork the repository and clone locally `git clone https://github.com/weimeilin79/cupcakefanatic.git`
2. Set up a serverless function with [Pusher](https://pusher.com/channels/) Channel for WebSocket communication.
3. Create an app in Pusher,copy the application key
```
app_id = "<YOUR_PUSHER_APP_ID>"
export key = "<YOUR_PUSHER_KEY>"
export secret = "<YOUR_PUSHER_SECRET>"
export cluster = "<YOUR_PUSHER_CLUSTER>"
```
![Pusher step](img/pusher.png)

1. In `index.html` replace the PUSHER_APP_ID and PUSHER_CLUSTER with your <YOUR_PUSHER_APP_ID> and <YOUR_PUSHER_CLUSTER>
2. Import the project, select `cupcakefanatic` as your repo and choose `cupcake-pusher` as the root directory.
3. Push the code to your Github repository.
4. Linking your GitHub repository with Vercel.
5. In vercel, in `Import Project` and add the repo from your git repository, click `Import`
8. Select as `cupcake-pusher` Root Directory
9. In *Environment Variables* enter the following setting with your own configuration
```
PUSHER_APP_ID=<YOUR_PUSHER_APP_ID>
PUHSER_APP_KEY=<YOUR_PUSHER_KEY>
PUHSER_APP_SECRET=<YOUR_PUSHER_SECRET>
UHSER_APP_CLUSTER=<YOUR_PUSHER_CLUSTER>
SERVERLESSBROKER=<YOUR_SERVERLESS_BOOSTRAP_URL>
RPUSER=<USER_YOU_CREATED_IN_RP_SERVERLESS>
RPPWD=<USER_YOU_CREATED_IN_RP_PWD>

```
![Pusher step](img/vercel.png)
10. Click deploy to start.


## Setting Up and Start the Simulator
1. Navigate to the `simulator` folder.
2. Ensure Python is installed on your system.
3. Install the Python dependencies listed in the requirements.txt file by running `pip install -r requirements.txt`.
4. Start the simulator by running `python simulator.js`

## Video
[![Video](https://img.youtube.com/vi/MKLOcFopKXM/0.jpg)](https://www.youtube.com/watch?v=MKLOcFopKXM)
185 changes: 185 additions & 0 deletions frontend-vercel/cupcake-pusher/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<!DOCTYPE html>
<html>
<head>
<title>Real-time Cupcake Store Map</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>
<script src="https://js.pusher.com/8.2.0/pusher.min.js"></script>
<style>
.toast-warning {
font-family: 'Arial', Arial, monospace;
font-size: 16px;
}
/* ... */
h1 {
font-family: Verdana, Geneva, sans-serif;
}
#container {
display: flex;
}
#map {
flex: 3;
height: 1200px;
}
table{
font-family: Verdana, Geneva, sans-serif;
border: 1px solid #FFFFFF;
width: 350px;
height: 200px;
text-align: center;
border-collapse: collapse;
}
table td, table th {
border: 1px solid #FFFFFF;
padding: 3px 2px;
}
tbody td {
font-size: 13px;
}
table tr:nth-child(even) {
background: #D0E4F5;
}
table thead {
background: #0B6FA4;
border-bottom: 5px solid #FFFFFF;
}
table thead th {
font-size: 13px;
font-weight: bold;
color: #FFFFFF;
text-align: center;
border-left: 2px solid #FFFFFF;
}
table thead th:first-child {
border-left: none;
}

.inventory-zero {
color: red;
}


#toast-container {
top: auto !important;
right: 12px !important;
bottom: 12px !important;
}
</style>
</head>
<body>
<h1>The Ultimate Crumb Quest for Cupcakes!!</h1>
<div id="container">
<div id="map"></div>
<table id="table">
<thead>
<tr>
<th>Store</th>
<th>Blueberry</th>
<th>Strawberry</th>
</tr>
</thead>
<tbody id="tableBody">
<!-- Rows will be added here dynamically -->
</tbody>
</table>
</div>
<script>
var map = L.map('map').setView([40.774389, -73.952049], 12);
var blueIcon = new L.Icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-blue.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});

var redIcon = new L.Icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});

// Create an gold icon
var goldIcon = new L.Icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-gold.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});

var markers = {};

// Enable pusher logging - don't include this in production
Pusher.logToConsole = true;

var pusher = new Pusher('YOUR_PUSHER_APP_ID', {
cluster: 'YOUR_PUSHER_APP_CLUSTER'
});
var channel = pusher.subscribe('my-channel');

channel.bind('cupcake-inv', function(data) {
//console.log("FROM PUSHER cupcake-->"+JSON.stringify(data));
var inventory = data;
tableBody.innerHTML = '';

// Update markers on the map
for (var store in inventory) {
var storeData = inventory[store];

// Check if inventory is zero

//console.log("storeData.lat->"+storeData.lat)
//console.log("storeData.lng->"+storeData.lng)
if (markers[store]) {
// Update existing marker
markers[store].setLatLng([storeData.lat, storeData.lng])
.setPopupContent(`<b>${storeData.store}</b><br>Blueberry: ${storeData.blueberry}<br>Strawberry: ${storeData.strawberry}`);
} else {
// Create new marker
markers[store] = L.marker([storeData.lat, storeData.lng]).addTo(map)
.bindPopup(`<b>${storeData.store}</b><br>Blueberry: ${storeData.blueberry}<br>Strawberry: ${storeData.strawberry}`);
}

markers[store].setIcon(blueIcon);

var row = tableBody.insertRow();
row.insertCell().textContent = storeData.store;
row.insertCell().textContent = storeData.blueberry;
row.insertCell().textContent = storeData.strawberry;

var isInventoryZero = storeData.blueberry === 0 || storeData.strawberry === 0;
if (isInventoryZero) {
// Change marker color to red
markers[store].setIcon(redIcon);
// Change row color to red
row.classList.add('inventory-zero');
}

if (storeData.latest) {
markers[store].setIcon(goldIcon);
// Warning only if inventory is zero and it is the latest data
if (isInventoryZero)
toastr.warning(`Inventory is zero for store: ${storeData.store}`, null);

}


}
});

L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
</script>
</body>
</html>
58 changes: 58 additions & 0 deletions frontend-vercel/cupcake-pusher/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const express = require('express');
const {Kafka} = require("kafkajs")

let { inventory } = require('./mapview.js');

const app = express();

const redpanda = new Kafka({
clientId: 'store-app',
brokers: [process.env.SERVERLESSBROKER],
ssl: {},
sasl: {
mechanism: "scram-sha-256",
username: process.env.RPUSER,
password: process.env.RPPWD
}
})

const producer = redpanda.producer()

app.use(express.json());

app.post('/submit', async function(req, res) {
const { store, blueberry, strawberry } = req.body;
console.log('Sending message:', { store, blueberry, strawberry });

await producer.connect();
await producer.send({
topic: 'inv-count',
messages: [
{ value: JSON.stringify({ store, blueberry, strawberry }) },
],
});
await producer.disconnect();

res.json({ status: 'success' });
});

app.post('/clean-inventory', function(req, res) {
inventory = {};
res.json({ status: 'success' });
});

app.get('/', function(req, res) {
res.sendFile(__dirname + '/index.html');
});

app.get('/input', function(req, res) {
res.sendFile(__dirname + '/input.html');
});

app.get('/input.js', function(req, res) {
res.sendFile(__dirname + '/input.js');
});

app.listen(3000, function() {
console.log('App listening on port 3000!');
});
21 changes: 21 additions & 0 deletions frontend-vercel/cupcake-pusher/input.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<title>Input Yogurt Number</title>
</head>
<body>
<form id="yogurt-form">
<label for="store">Store:</label><br>
<input type="text" id="store" name="store"><br>
<label for="blueberry">Blueberry Yogurt Number:</label><br>
<input type="number" id="blueberry" name="blueberry"><br>
<label for="strawberry">Strawberry Yogurt Number:</label><br>
<input type="number" id="strawberry" name="strawberry"><br>
<input type="submit" value="Submit">
</form>

<button id="clean-button">Clean Inventory</button>
<script src="input.js"></script>

</body>
</html>
Loading