Swappa PWA Implementation Guide

This document contains all the files needed to add PWA support to swappa.com with a bottom navigation bar that only appears when the app is installed.


1. manifest.json

Location: /manifest.json (root/public directory)

{
  "name": "Swappa",
  "short_name": "Swappa",
  "description": "Buy and sell used tech",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#FFFFFF",
  "theme_color": "#008339",
  "icons": [
    {
      "src": "/icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "maskable"
    }
  ]
}

2. sw.js (Service Worker)

Location: /sw.js (root/public directory)

const CACHE_NAME = 'swappa-pwa-v1';

// Install event - cache minimal assets
self.addEventListener('install', (event) => {
  self.skipWaiting();
});

// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames
          .filter((name) => name !== CACHE_NAME)
          .map((name) => caches.delete(name))
      );
    })
  );
  self.clients.claim();
});

// Fetch event - network first, fallback to cache
self.addEventListener('fetch', (event) => {
  event.respondWith(
    fetch(event.request).catch(() => caches.match(event.request))
  );
});

3. pwa-navbar.css

Location: /css/pwa-navbar.css (or add to existing CSS)

/* PWA Bottom Navigation Bar - Only visible when installed */
.pwa-navbar {
  display: none;
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  background-color: #D14900;
  z-index: 99999;
  padding-bottom: env(safe-area-inset-bottom, 0);
}

.pwa-navbar__container {
  display: flex;
  flex-direction: row;
  height: 56px;
}

.pwa-navbar__button {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  padding: 4px 0;
  cursor: pointer;
  color: #FFFFFF;
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}

.pwa-navbar__button:active {
  opacity: 0.7;
}

.pwa-navbar__button--disabled {
  opacity: 0.4;
  pointer-events: none;
}

.pwa-navbar__icon {
  font-size: 20px;
}

.pwa-navbar__label {
  font-size: 12px;
  margin-top: 2px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

/* Only show in standalone/PWA mode */
@media (display-mode: standalone) {
  .pwa-navbar {
    display: block;
  }

  /* Add padding to body so content isn't hidden behind navbar */
  body {
    padding-bottom: calc(56px + env(safe-area-inset-bottom, 0)) !important;
  }
}

/* iOS standalone detection fallback */
body.pwa-standalone .pwa-navbar {
  display: block;
}

body.pwa-standalone {
  padding-bottom: calc(56px + env(safe-area-inset-bottom, 0)) !important;
}

4. pwa-navbar.js

Location: /js/pwa-navbar.js (or add to existing JS)

(function() {
  'use strict';

  // Detect if running as installed PWA
  const isStandalone = window.matchMedia('(display-mode: standalone)').matches
                    || window.navigator.standalone === true;

  if (isStandalone) {
    document.body.classList.add('pwa-standalone');
  }

  // Track navigation history for back button
  let canGoBack = false;

  window.addEventListener('popstate', () => {
    canGoBack = window.history.length > 1;
    updateBackButton();
  });

  // Check initial state
  canGoBack = window.history.length > 1;

  function updateBackButton() {
    const backBtn = document.getElementById('pwa-back-btn');
    if (backBtn) {
      if (canGoBack) {
        backBtn.classList.remove('pwa-navbar__button--disabled');
      } else {
        backBtn.classList.add('pwa-navbar__button--disabled');
      }
    }
  }

  function handleBack() {
    if (window.history.length > 1) {
      window.history.back();
    }
  }

  function handleMenu() {
    const menuBtn = document.querySelector('[data-bs-target="#slide_menu"]');
    if (menuBtn) menuBtn.click();
  }

  function handleHome() {
    window.location.href = '/';
  }

  function handleSearch() {
    const searchBtn = document.querySelector('[data-bs-target="#slide_search"]');
    if (searchBtn) searchBtn.click();
  }

  function handleCart() {
    const cartBtn = document.querySelector('[data-bs-target="#slide_cart"]');
    if (cartBtn) cartBtn.click();
  }

  // Create and inject navbar when DOM is ready
  function createNavbar() {
    // Don't create if already exists
    if (document.getElementById('pwa-navbar')) return;

    const navbar = document.createElement('nav');
    navbar.id = 'pwa-navbar';
    navbar.className = 'pwa-navbar';
    navbar.innerHTML = `
      <div class="pwa-navbar__container">
        <button id="pwa-back-btn" class="pwa-navbar__button pwa-navbar__button--disabled" aria-label="Go back">
          <i class="fa fa-chevron-left pwa-navbar__icon"></i>
          <span class="pwa-navbar__label">Back</span>
        </button>
        <button id="pwa-menu-btn" class="pwa-navbar__button" aria-label="Open menu">
          <i class="fa fa-bars pwa-navbar__icon"></i>
          <span class="pwa-navbar__label">Menu</span>
        </button>
        <button id="pwa-home-btn" class="pwa-navbar__button" aria-label="Go home">
          <i class="fa fa-home pwa-navbar__icon"></i>
          <span class="pwa-navbar__label">Home</span>
        </button>
        <button id="pwa-search-btn" class="pwa-navbar__button" aria-label="Search">
          <i class="fa fa-search pwa-navbar__icon"></i>
          <span class="pwa-navbar__label">Search</span>
        </button>
        <button id="pwa-cart-btn" class="pwa-navbar__button" aria-label="Cart">
          <i class="fa fa-shopping-cart pwa-navbar__icon"></i>
          <span class="pwa-navbar__label">Cart</span>
        </button>
      </div>
    `;

    document.body.appendChild(navbar);

    // Attach event listeners
    document.getElementById('pwa-back-btn').addEventListener('click', handleBack);
    document.getElementById('pwa-menu-btn').addEventListener('click', handleMenu);
    document.getElementById('pwa-home-btn').addEventListener('click', handleHome);
    document.getElementById('pwa-search-btn').addEventListener('click', handleSearch);
    document.getElementById('pwa-cart-btn').addEventListener('click', handleCart);

    // Update back button state
    updateBackButton();
  }

  // Initialize when DOM is ready
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', createNavbar);
  } else {
    createNavbar();
  }
})();

5. HTML Head Additions

Location: Add to <head> on all pages

<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.json">

<!-- Theme color for browser chrome -->
<meta name="theme-color" content="#008339">

<!-- iOS PWA settings -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Swappa">
<link rel="apple-touch-icon" href="/icons/icon-192.png">

<!-- PWA Navbar assets -->
<link rel="stylesheet" href="/css/pwa-navbar.css">
<script src="/js/pwa-navbar.js" defer></script>

<!-- Register service worker -->
<script>
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/sw.js');
  }
</script>

6. Required Icons

Create these icon files:

File Size Location
icon-192.png 192x192 /icons/icon-192.png
icon-512.png 512x512 /icons/icon-512.png

File Summary

File Location
manifest.json /manifest.json
sw.js /sw.js
pwa-navbar.css /css/pwa-navbar.css
pwa-navbar.js /js/pwa-navbar.js
Icons /icons/icon-192.png, /icons/icon-512.png

How It Works

  1. The CSS uses @media (display-mode: standalone) to detect when the site is running as an installed PWA
  2. The navbar is hidden by default (display: none)
  3. When installed as a PWA, the media query activates and shows the navbar
  4. JavaScript adds a fallback class pwa-standalone for iOS Safari compatibility
  5. Body padding is automatically added to prevent content from being hidden behind the navbar

Testing

  1. Chrome DevTools: Open DevTools > Application > Manifest to verify the manifest is loaded
  2. Simulate standalone mode: In DevTools > Rendering > Emulate CSS media feature display-mode: standalone
  3. Install test: On mobile, use "Add to Home Screen" to test the actual installed experience