Building a PWA: Offline-Capable Customer Feedback App

Vivek Moradiya
6 min readJul 20, 2024

--

Progressive Web Apps (PWAs) offer a seamless, app-like experience on the web. They are designed to work on any browser and provide advanced features like offline capabilities, push notifications, and more. In this blog, we’ll dive into developing a PWA with advanced caching strategies and offline capabilities using Service Workers and IndexedDB.

What is a PWA?

A PWA is a type of web application that uses modern web capabilities to deliver an app-like experience to users. Key features include:

  • Installability: Users can install PWAs on their home screen.
  • Offline Functionality: PWAs can work without an internet connection.
  • Push Notifications: PWAs can send notifications to users.
  • Responsive Design: PWAs adapt to different screen sizes and orientations.

What is Offline Cache?

Offline cache refers to storing resources such as HTML, CSS, JavaScript, and images locally on the user’s device, so the application can be used even when the device is not connected to the internet. This is typically achieved using technologies like Service Workers and the Cache API. The cache stores these resources during the initial load, allowing the application to serve them directly from the local storage when the user is offline.

Where is Offline Cache Used?

Offline cache is widely used in various applications to enhance the user experience by enabling offline access and faster load times. Some common use cases include:

  • E-commerce platforms: Allowing users to browse products and complete transactions even with poor connectivity.
  • News and content apps: Providing access to previously loaded articles and media.
  • Travel apps: Offering maps and itinerary details without requiring an internet connection.
  • Productivity tools: Ensuring that documents, notes, and tasks are accessible offline.

Offline-Capable Customer Feedback App

Our application is a customer feedback collection tool designed to work seamlessly both online and offline. Here’s how it works:

Step 1: Setting Up the Project

feedback-app/
|-- index.html
|-- style.css
|-- index.js
|-- manifest.json
|-- service-worker.js

Create a manifest.json file in your project's root directory:

{
"name": "Feedback App",
"short_name": "Feedback",
"start_url": "/index.html",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#007BFF",
"icons": [
{
"src": "icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

Step 2: Registering the Service Worker

self.addEventListener('install', event => {
event.waitUntil(
caches.open('feedback-app-cache-v1').then(cache => {
return cache.addAll([
'/',
'/index.html',
'/style.css',
'/index.js',
'/manifest.json'
]);
})
);
});

self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});

Step 3: Update the HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Customer Feedback App</title>
<link rel="stylesheet" href="style.css">
<link rel="manifest" href="manifest.json">
</head>
<body>
<div class="container">
<div id="status"></div>
<h1>Customer Feedback App</h1>
<form id="feedback-form">
<label for="name">Name:</label>
<input type="text" id="name" required>
<label for="feedback">Feedback:</label>
<textarea id="feedback" required></textarea>
<button type="submit">Submit</button>
</form>
<div id="status-success"></div>
<h2>Cached Feedback Records</h2>
<ul id="cached-feedback"></ul>
</div>
<script src="index.js"></script>
</body>
</html>

Step 4: Adding Styling to HTML

body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}

.container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

h1, h2 {
color: #333;
}

form {
margin-bottom: 20px;
}

input[type="text"],
textarea {
padding: 10px;
width: calc(100% - 22px);
margin-bottom: 10px;
}

button {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}

button:hover {
background-color: #0056b3;
}

#status, #status-success {
margin-bottom: 20px;
font-weight: bold;
color: green;
}

#cached-feedback {
list-style: none;
padding: 0;
}

#cached-feedback li {
padding: 10px;
border-bottom: 1px solid #ccc;
display: flex;
justify-content: space-between;
align-items: center;
}

#cached-feedback button {
padding: 5px 10px;
background-color: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}

#cached-feedback button:hover {
background-color: #218838;
}
How our application looks after adding styles.

Step 5: Write a Javascript code for offline application

IndexedDB is a powerful, low-level API for client-side storage. We start by opening an IndexedDB database named feedback-db and creating an object store called feedback to hold our feedback records.

let db;

const request = indexedDB.open("feedback-db", 1);

request.onupgradeneeded = (event) => {
db = event.target.result;
db.createObjectStore("feedback", { keyPath: "id", autoIncrement: true });
};

We handle success and error events for the IndexedDB request. On success, we display cached feedback records. On error, we log the error message to the console.

request.onsuccess = (event) => {
db = event.target.result;
displayCachedFeedback();
};

request.onerror = (event) => {
console.error("IndexedDB error:", event.target.errorCode);
};

We add an event listener to handle form submissions. Depending on whether the user is online or offline, we either save the feedback data offline or send it to the server.

While Offline This is how listing look.
document.getElementById("feedback-form").addEventListener("submit", (event) => {
event.preventDefault();
const name = document.getElementById("name").value;
const feedback = document.getElementById("feedback").value;
const feedbackData = { name, feedback, timestamp: new Date().toISOString() };

if (navigator.onLine) {
sendFeedbackToServer(feedbackData);
} else {
saveFeedbackOffline(feedbackData);
}
});

If the user is offline, we save the feedback data in IndexedDB

function saveFeedbackOffline(feedback) {
const transaction = db.transaction(["feedback"], "readwrite");
const store = transaction.objectStore("feedback");
store.add(feedback);
transaction.oncomplete = () => {
document.getElementById("status").innerText = "Feedback saved offline.";
displayCachedFeedback();
};
transaction.onerror = (event) => {
console.error("Error saving feedback offline:", event.target.errorCode);
};
}

For simplicity, we use a placeholder function for uploading feedback data to the server. That we can modify as per our convenience

function sendFeedbackToServer(feedback) {
// Upload Logic Should go here
console.log(feedback);
document.getElementById("status-success").innerText =
"Feedback submitted to server.";
setTimeout(() => {
document.getElementById("status-success").innerText = "";
}, 3000);
}

We fetch and display cached feedback records from IndexedDB.

function displayCachedFeedback() {
const transaction = db.transaction(["feedback"], "readonly");
const store = transaction.objectStore("feedback");
const getAllRequest = store.getAll();

getAllRequest.onsuccess = (event) => {
const feedbacks = event.target.result;
const list = document.getElementById("cached-feedback");
list.innerHTML = "";
feedbacks.forEach((feedback) => {
const listItem = document.createElement("li");
listItem.textContent = `${feedback.name}: ${feedback.feedback} - ${feedback.timestamp}`;
if (navigator.onLine) {
const uploadButton = document.createElement("button");
uploadButton.textContent = "Upload";
uploadButton.onclick = () => {
sendFeedbackToServer(feedback);
removeFeedbackFromCache(feedback.id);
};
listItem.appendChild(uploadButton);
}
list.appendChild(listItem);
});
};

setTimeout(() => {
document.getElementById("status-success").innerText = "";
}, 3000);

getAllRequest.onerror = (event) => {
console.error("Error retrieving cached feedback:", event.target.errorCode);
};
}

When we get online, we get a option to upload feedback to server. After uploading the feedback data to the server, we remove the corresponding record from IndexedDB.

function removeFeedbackFromCache(id) {
const transaction = db.transaction(["feedback"], "readwrite");
const store = transaction.objectStore("feedback");
store.delete(id);
transaction.oncomplete = displayCachedFeedback;
transaction.onerror = (event) => {
console.error("Error removing cached feedback:", event.target.errorCode);
};
}

We can add a users internet status.

document.addEventListener("DOMContentLoaded", () => {
document.getElementById("status").innerText = navigator.onLine
? "You are online."
: "You are offline.";
displayCachedFeedback();
});

You can all stored offline data inside browser.

Conclusion

By following these steps, you’ve created an offline-capable customer feedback app that stores data in IndexedDB when offline and syncs with the server when online. With the addition of some CSS styles, your app is not only functional but also visually appealing.

--

--

Vivek Moradiya
Vivek Moradiya

Written by Vivek Moradiya

🚀 Full-stack developer specializing in the MERN stack – an expert in MongoDB, Express.js, React, Next Js and Node.js.

No responses yet