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.
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"
}
]
}
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))
);
});
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;
}
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();
}
})();
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>
Create these icon files:
| File | Size | Location |
|---|---|---|
icon-192.png |
192x192 | /icons/icon-192.png |
icon-512.png |
512x512 | /icons/icon-512.png |
| 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 |
@media (display-mode: standalone) to detect when the site is running as an installed PWAdisplay: none)pwa-standalone for iOS Safari compatibilitydisplay-mode: standalone