Learn to Make an Endorsement PWA App: Beginner's Guide

Photo by Adam Jang on Unsplash

Learn to Make an Endorsement PWA App: Beginner's Guide

Β·

15 min read

This is part of a solo project from The Frontend Developer Career Path at Scrimba. I have enjoyed learning with them and would like to share what I have learnt with you. I hope you enjoy coding with me. Let's go!


An endorsement is a powerful way to acknowledge the people that you work with. This allows people to spread positivity in the work place.

A progressive web app (PWA) is a type of application software delivered through the web, built using common web technologies such as HTML, CSS and JavaScript. PWAs are intended to work on any platform that uses a standards-compliant browser, in both desktops and mobile devices.

In this post, we will go step by step through the creation of the application, starting with the HTML, then CSS for styling, JavaScript for interactivity and Firebase for data storage.

The HTML Structure

Let's start with the skeleton of the application. We will use HTML file named index.html.

<!doctype html>
<html>
    <head>
        <link rel="preconnect" href="https://fonts.googleapis.com">
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
        <link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&family=Josefin+Sans:ital,wght@0,100..700;1,100..700&display=swap" rel="stylesheet">
        <link rel="stylesheet" href="index.css">
        <title>We are the Champions</title>
    </head>
    <body>
        <div class="container">
            <img src="./assets/freddie.png" alt="Freddie">
            <h1>We are the Champions</h1>
            <textarea id="endorsement-text" rows="10" cols="30" placeholder="Write your endorsement here"></textarea>
            <button id="endorse-btn">Publish</button>
            <h2>- Endorsements -</h2>
            <ul id="endorsement-list">
                <li>Hi Bob! Your React Router course is so good. :) πŸ”₯ Alice</li>
                <li>Oscar! That transcription feature you completed for Scrimba 3.0 is amazing. πŸ‘πŸ‘πŸ‘ Really good work πŸ™Œ From Henry</li>
                <li>Marianne! Thank you so much for helping me with the March accounting. Saved so much time because of you! πŸ’œ Freddy</li>
            </ul>
        </div>
        <script src="index.js"></script>
    </body>
</html>

We have the absolute skeleton of the application. The entire content of the body will be housed within a div with class container. This will later be useful in organizing the HTML elements. Within the unordered list with id endorsements, we have three list elements as placeholders. We will later remove these and have them displayed dynamically.

Next, Styling

To begin with, let's import the necessary fonts in the head section of index.html.

<head>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&family=Josefin+Sans:ital,wght@0,100..700;1,100..700&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="index.css">
    <title>We are the Champions</title>
</head>

Then in index.css let's add some styling:

:root {
    --dark-color: #1B1924;
    --gray-color: #444059;
    --white-color: #FFFFFF;
    --gray-text-color: #8F8F8F;
    --blue-color: #28A9F1;
    --button-text-color: #04131C;
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    background-color: var(--dark-color);
    color: var(--white-color);
    font-family: "Josefin Sans", sans-serif;;
}

.container {
    max-width: 390px;
    margin: 30px auto;
    padding: 0 15px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 20px;
}

.container img {
    width: 90px;
    margin-top: 50px;
}

.container h1 {
    font-size: 1.5rem;
}

.container textarea {
    width: 320px;
    height: 120px;
    padding: 15px;
    background-color: var(--gray-color);
    color: var(--gray-text-color);
    font-family: "Inter", sans-serif;
    font-size: 1rem;
    border: none;
    border-radius: 8px;
}

.container button {
    width: 320px;
    height: 60px;
    background-color: var(--blue-color);
    color: var(--button-text-color);
    font-family: "Josefin Sans", sans-serif;
    font-weight: bold;
    font-size: 1.5rem;
    border: none;
    border-radius: 8px;
    cursor: pointer;
}

.container ul {
    list-style: none;
    width: 320px;
}

.container ul li {
    font-size: 1.1rem;
    padding: 15px;
    color: var(--dark-color);
    background-color: var(--white-color);
}

.container ul li:not(:last-child) {
    margin-bottom: 15px;
}

.container ul .empty-list {
    background-color: var(--gray-color);
    color: var(--gray-text-color);
    text-align: center;
    font-weight: bold;
}

We are using CSS custom property variables to define the colors for the application. For the list items, we are applying 15 pixels of margin at the bottom, except for the last list item.

Let's see how the page looks with these additions:

Very good!

Adding functionality with JavaScript

In index.js file, let's get the DOM elements we need to interact with:

// Get DOM elements
const endoresementInputEl = document.getElementById("endorsement-text");
const endorseBtn = document.getElementById("endorse-btn");
const endorsementListEl = document.getElementById("endorsement-list");

Next, let's add an event listener for clicks on the button. We will use this to get the text in the input and update the endorsementListEl element with the new text.

// Add event listener to the button
endorseBtn.addEventListener("click", function() {
    const endorsement = endoresementInputEl.value;
    const li = document.createElement("li");

    li.textContent = endorsement;
    endorsementListEl.appendChild(li);
});

Let's test and see the output:

Awesome work! You can see the text is appended to the end of the list. But wait a minute! We have a problem. A few actually. Have you noticed them?

  • When we click on the button, the input is not cleared. We are forced to manually clear it to enter new text.

  • When the button is pressed and the input is empty, endorsementListEl is updated with an empty list item. It should only be updated when there is text in the input.

  • When the browser updates, we lose the new endorsements.

Alright, let's deal with the first two. To clear the input on submission, we can simply pass an empty value to endoresementInputEl once the endorsement is submitted.

endoresementInputEl.value = "";

To prevent empty submissions, we can use an if statement to check if there is an endorsement value. If so, then we add the list element to endorsementListEl.

if (endorsement) {
    const li = document.createElement("li");
    li.textContent = endorsement;
    endorsementListEl.appendChild(li);
}

So now the list will only be updated if the user enters some text in the input. The function is now updated:

// Get DOM elements
const endoresementInputEl = document.getElementById("endorsement-text");
const endorseBtn = document.getElementById("endorse-btn");
const endorsementListEl = document.getElementById("endorsement-list");

// Add event listener to the button
endorseBtn.addEventListener("click", function() {
    const endorsement = endoresementInputEl.value;

    if (endorsement) {
        const li = document.createElement("li");
        li.textContent = endorsement;
        endorsementListEl.appendChild(li);
        endoresementInputEl.value = "";
    }
});

Before we move on, we need to tidy up the addEventListener callback function. It would be better to break it up into smaller, manageable functions that handle specific tasks. In this case, we would ideally have two functions to clear the input and to update the endorsement list. Let's do that.

// Get DOM elements
const endoresementInputEl = document.getElementById("endorsement-text");
const endorseBtn = document.getElementById("endorse-btn");
const endorsementListEl = document.getElementById("endorsement-list");

// Add event listener to the button
endorseBtn.addEventListener("click", function() {
    const endorsement = endoresementInputEl.value;

    if (endorsement) {
        updateEndorsementListEl(endorsement);
        clearEndoresementInputEl();
    }
});

/**
 * Function to clear the input field
 * @returns {void}
 */
const clearEndoresementInputEl = () => {
    endoresementInputEl.value = "";
}

/**
 * @param {string} endorsement 
 * @returns {void}
 */
const updateEndorsementListEl = (endorsement) => {
    const listEl = document.createElement("li");
    listEl.textContent = endorsement;
    endorsementListEl.appendChild(listEl);
}

Awesome! Now let's talk about the third problem: persisting our data. For this we will use Firebase, an application development platform provided by Google.

Firebase setup

Head over to Firebase console and sign in using your Google account. Click on Add project to create a new project.

Let's name the project Champions, although you can choose any name you prefer.

On the next page, disable Google analytics and click on Create project.

After a few minutes, your Firebase project is ready. Click continue.

On the left side of the page, under the Build tab, select Realtime Database. We are going to create a Realtime database to store our data.

On the next page, click the Create Database button.

Next, on the pop-up prompt, select Realtime database location, preferably one closest to you and click Next.

For the security rules, select Start in Test mode. We will change these settings later to prevent the public from posting to it. Click Enable.

Your Realtime database is now up and ready. Your page should look something like this:

You will have noticed the warning message in the Security rules setup that anyone with your database reference to view, delete and modify all data in your database from the time of creation up to 30 days. For now, let's set read and write modes to true. You can later change the write mode to false so others cannot write to your database. Click on the Rules tab. Set the read and write rules to true. Then click Publish:

We will be using the database reference URL to connect to the database from our environment. Go back to the Data tab and copy the database reference URL.

Connecting to Firebase

Back to our local environment, we need to now setup some configurations so that our local application can post and fetch data into Firebase as needed. At the top of index.js, add these lines:

import { initializeApp } from "https://www.gstatic.com/firebasejs/9.15.0/firebase-app.js";
import { getDatabase, ref } from "https://www.gstatic.com/firebasejs/9.15.0/firebase-database.js";

const appSettings = {
    databaseURL: "https://champions-e8ee8-default-rtdb.europe-west1.firebasedatabase.app/"
};

const app = initializeApp(appSettings);
const database = getDatabase(app);
const endorsementListInDB = ref(database, "endorsements");

First, we import a Firebase function called initializeApp from a Firebase JavaScript file hosted at Firebase. Next, we import getDatabase and ref from a Firebase database script.

After the imports, we have a variable appSettings that is an object used to set up databaseURL containing the database reference URL we copied earlier from Firebase console.

Next, we have app that uses initializeApp with appSettings defined earlier to connect to the database.

The getDatabase function is then used to allow communication with our database. We pass in app as an argument.

Finally, we create a reference for where in the database we will be storing our data. We imported ref for this purpose. ref takes two arguments: first, the database we are working with (in this case database that we created earlier, and second should be what the reference should be called (in this case "endorsements").

Now, because we are importing functions from another file and using it in our JavaScript file, the app won't work as expected. To test this, console.log the value of app created earlier:

...
const app = initializeApp(appSettings);
const database = getDatabase(app);
const endorsementListInDB = ref(database, "endorsements");

console.log(app);

On the browser, inspect the page and open the console tab. You will see an error message saying we cannot use an import statement outside a module.

To mitigate this, we need to tell index.html to import our JavaScript file as a module. So let's do that.

        ...
        <script src="index.js" type="module"></script>
    </body>
</html>

Save the file and refresh the page. You will notice that the error message is gone. Instead we get a Firebase implementation object.

Great! You can now remove the console.log statement.

Pushing data into our Firebase database

To push data to our Firebase database is quite simple. First, we need to import the push function from Firebase:

...
import { getDatabase, ref, push } from "https://www.gstatic.com/firebasejs/9.15.0/firebase-database.js";
...

Then, we use this push function to push data to Firebase database. This function takes two arguments: the reference and the value that we want to post.

if (endorsement) {
    push(endorsementListInDB, endorsement);
    updateEndorsementListEl(endorsement);
    clearEndoresementInputEl();
}

Simple as that! Our app now posts data to the Firebase database:

You can see the reference name endorsements. When you expand it, you will see your first endorsement with its ID. Great! Now we move to the next challenge.

Fetching our data using onValue

When the browser refreshes, the list elements appended to the unordered list (along with their data) disappear. Up to now, we have no way of displaying data within the database. This is a good time to remove the dummy data from our HTML. Let's update index.html.

<h2>- Endorsements -</h2>
<ul id="endorsement-list"></ul>

Back in index.js, we will import yet another function to fetch the data: onValue:

...
import { getDatabase, ref, push, onValue } from "https://www.gstatic.com/firebasejs/9.15.0/firebase-database.js";
...

onValue listens for data changes at a particular location (the reference). A callback function will be triggered for the initial data and again whenever the data changes.

// Listen for changes in the database
onValue(endorsementListInDB, function(snapshot) {
    const data = Object.entries(snapshot.val());
    endorsementListEl.innerHTML = "";

    for (let i = 0; i < data.length; i++) {
        let endorsement = data[i]
        updateEndorsementListEl(endorsement);
    }
});

...

/**
 * @param {array} endorsement 
 * @returns {void}
 */
const updateEndorsementListEl = (endorsement) => {
    let [endorsementID, endorsementText] = endorsement;
    const listEl = document.createElement("li");

    listEl.id = endorsementID;
    listEl.textContent = endorsementText;
    endorsementListEl.appendChild(listEl);
}

onValue receives two arguments: the reference and a callback function. The callback function receives a snapshot object of all the values in the reference at the time. Inside the callback, we are using Object.entries() to get a key-value pair of each ID and its corresponding value from the snapshot.

Back to our code, after we extract the data, we need to clear the endorsement list unordered list of any old values. Using a for loop, we get each key-value pair and pass it on to updateEndorsementListEl function.

updateEndorsementListEl function destructures the ID and text then creates a new list element for each endorsement. The list element is updated with the ID and value attributes. Finally the list element is appended to the unordered list.

Final touches

Now that the onValue function calls on updateEndorsementListEl, remove updateEndorsementListEl from endorseBtn.addEventListener function.

Also, we need to display a default message, in the event the database does not have any data. We use snapshot.exists() to check if there is data present in the snapshot. Here is the updated code for index.js:

import { initializeApp } from "https://www.gstatic.com/firebasejs/9.15.0/firebase-app.js";
import { getDatabase, ref, push, onValue } from "https://www.gstatic.com/firebasejs/9.15.0/firebase-database.js";

const appSettings = {
    databaseURL: "https://champions-e8ee8-default-rtdb.europe-west1.firebasedatabase.app/"
};

const app = initializeApp(appSettings);
const database = getDatabase(app);
const endorsementListInDB = ref(database, "endorsements");

// Get DOM elements
const endoresementInputEl = document.getElementById("endorsement-text");
const endorseBtn = document.getElementById("endorse-btn");
const endorsementListEl = document.getElementById("endorsement-list");

// Add event listener to the button
endorseBtn.addEventListener("click", function() {
    const endorsement = endoresementInputEl.value;

    if (endorsement) {
        push(endorsementListInDB, endorsement);
        clearEndoresementInputEl();
    }
});

// Listen for changes in the database
onValue(endorsementListInDB, function(snapshot) {
    if (snapshot.exists()) {
        const data = Object.entries(snapshot.val());

        clearEndorsementListEl();

        for (let i = 0; i < data.length; i++) {
            let endorsement = data[i];
            updateEndorsementListEl(endorsement);
        }
    } else {
        endorsementListEl.innerHTML = "<li class='empty-list'>No endorsements yet</li>";
    }
});

/**
 * Function to clear the endorsement list
 * @returns {void}
 */
function clearEndorsementListEl() {
    endorsementListEl.innerHTML = "";
}

/**
 * Function to clear the input field
 * @returns {void}
 */
const clearEndoresementInputEl = () => {
    endoresementInputEl.value = "";
}

/**
 * @param {array} endorsement 
 * @returns {void}
 */
const updateEndorsementListEl = (endorsement) => {
    let [endorsementID, endorsementText] = endorsement;
    const listEl = document.createElement("li");

    listEl.id = endorsementID;
    listEl.textContent = endorsementText;
    endorsementListEl.appendChild(listEl);
}

Now when the database is empty, we have a default message:

When new data is posted, the UI reflects automatically:

Making the app mobile friendly

To make the app display correctly on a mobile device, we need to add a meta tag that sets the viewport of any device we are using. in index.html, add this piece of code to the head:

<meta name="viewport" content="width=device-width, initial-scale=1.0">

This way, the app will not look shrunken on a mobile device with a smaller screen.

Adding a Favicon

A favicon is a small 16x16 pixel icon used on web browsers to represent a website. They are normally displayed on tabs at the top of browsers. Let's add on for our app. You can choose between an image, text or emoji to generate a favicon. Head over to favicon.io and select on of the three options:

Once generated, download the zip file and extract the images into your local environment. Copy the link tags provided and paste them into the head of your HTML. Make sure to update links so the href looks into the correct location:

See example implementation:

<head>
    <!-- Meta tags -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <!-- Favicon -->
    <link rel="apple-touch-icon" sizes="180x180" href="./assets/apple-touch-icon.png">
    <link rel="icon" type="image/png" sizes="32x32" href="./assets/favicon-32x32.png">
    <link rel="icon" type="image/png" sizes="16x16" href="./assets/favicon-16x16.png">
    <link rel="manifest" href="./assets/site.webmanifest">

    <!-- Fonts -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&family=Josefin+Sans:ital,wght@0,100..700;1,100..700&display=swap" rel="stylesheet">

    <!-- Styles -->
    <link rel="stylesheet" href="index.css">

    <title>We are the Champions</title>
</head>

The favicon should show on the browser tab along with the title of the app:

You may be wondering what that site.webmanifest file is for. This is the magic file that will be used to display the icon on your mobile phone. You can add appropriate spacing so it looks lie a JavaScript object:

{
    "name":"We are the Champions",
    "short_name":"We are the Champions",
    "icons":[
        {
            "src":"/android-chrome-192x192.png",
            "sizes":"192x192","type":"image/png"
        },
        {
            "src":"/android-chrome-512x512.png",
            "sizes":"512x512","type":"image/png"
        }
    ],
    "theme_color":"#1B1924",
    "background_color":"#1B1924",
    "display":"standalone"
}

I have changed the values "name" and "short_name" from the initial blank values.

  • name is the name of the application that will be displayed to the user

  • short_name is displayed if there not enough space to display name

  • icons is an array of image files that serve as application icons.

  • theme_color defines the default theme color for the application

  • background_color is a placeholder for the application page to display before its stylesheet is loaded.

  • display determines the preferred display mode for the website. The standalone mode means the application will look and feel like a standalone application.

You can read more on MDN web docs.

Deployment

Before we can view our app on a mobile phone, we need to deploy it to the internet. There are many ways to do this, but for this demonstration, we will use Netlify.

You will need to sign up into Netlify, so choose whichever method you prefer. If you already have an account, go ahead and log in. Once logged in, click on Add new site button and then select Deploy manually. Drag and drop the entire folder containing your local files.

This should take only a few seconds. The application is now published. You can rename the application by clicking on Site configurations on the left:

Then click on Change site name button:

Save the name and access the published site. Congratulations!

Add app to home screen

For Android

Using your Chrome browser, navigate to the Netlify URL for your app. On the top right of the browser, tap the three vertical dots menu, then on the pop-up, tap on Add to Home screen.

You can choose to change the name to your preference. The app is now added to your home screen.

For iPhone

Open your default browser (Safari) and navigate to the Netlify URL you deployed to. Tap on the icon at the bottom center of the screen that looks like a box with an arrow pointing up:

Then select Add to Home Screen:

Set the display name and press Add.

Congratulations!

Next steps

Well done for getting this far! πŸ‘πŸΎπŸ‘πŸΎπŸ‘πŸΎ

Now that you've learnt these vital concepts, I encourage you to take it further. Here are some suggestion:

  • Add "From" and "To" input fields. The list should show who the message is from and to whom.

  • Reverse the order of the list items so that the latest endorsement comes first.

Good luck! πŸ‘πŸΎ


Let's connect:

Β