PWA Configuration

Setting up Progressive Web App features for BrewHoard

This guide covers configuring Progressive Web App (PWA) features for BrewHoard, enabling offline functionality and native app-like experiences.

Service Worker Setup

Create a service worker for caching and offline support:

JavaScript
// src/lib/pwa/service-worker.js
import { build, files, version } from '$service-worker';

const CACHE_NAME = `brewhoard-${version}`;
const ASSETS = [
  ...build,
  ...files
];

// Install event - cache assets
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(ASSETS);
    })
  );
  self.skipWaiting();
});

// Activate event - clean 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 - serve from cache or network
self.addEventListener('fetch', (event) => {
  if (event.request.method !== 'GET') return;
  
  event.respondWith(
    caches.match(event.request).then((cachedResponse) => {
      if (cachedResponse) {
        return cachedResponse;
      }
      
      return fetch(event.request).then((response) => {
        // Cache successful GET requests
        if (response.status === 200 && event.request.url.startsWith(self.location.origin)) {
          const responseClone = response.clone();
          caches.open(CACHE_NAME).then((cache) => {
            cache.put(event.request, responseClone);
          });
        }
        return response;
      });
    })
  );
});

Register the service worker in your app:

JavaScript
// src/hooks.client.js
import { dev } from '$app/environment';

if (!dev && 'serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js')
    .then((registration) => {
      console.log('Service worker registered:', registration);
    })
    .catch((error) => {
      console.log('Service worker registration failed:', error);
    });
}

Manifest.json

Create a web app manifest for PWA installation:

JSON
{
  "name": "BrewHoard",
  "short_name": "BrewHoard",
  "description": "Track and manage your beer collection",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#1f2937",
  "orientation": "portrait-primary",
  "categories": ["lifestyle", "productivity"],
  "lang": "en-US",
  "icons": [
    {
      "src": "/pwa-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/pwa-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ],
  "screenshots": [
    {
      "src": "/screenshot-mobile.png",
      "sizes": "390x844",
      "type": "image/png",
      "form_factor": "narrow"
    },
    {
      "src": "/screenshot-desktop.png",
      "sizes": "1280x720",
      "type": "image/png",
      "form_factor": "wide"
    }
  ]
}

Place this file in your static/ directory as manifest.json.

Offline Support

Implement offline functionality with cached data:

JavaScript
// src/lib/pwa/offline-manager.js
import { browser } from '$app/environment';

class OfflineManager {
  constructor() {
    if (browser) {
      this.db = null;
      this.initDB();
    }
  }

  async initDB() {
    if (!('indexedDB' in window)) return;
    
    const request = indexedDB.open('BrewHoardOffline', 1);
    
    request.onupgradeneeded = (event) => {
      const db = event.target.result;
      if (!db.objectStoreNames.contains('beers')) {
        db.createObjectStore('beers', { keyPath: 'id' });
      }
      if (!db.objectStoreNames.contains('collection')) {
        db.createObjectStore('collection', { keyPath: 'id' });
      }
    };
    
    request.onsuccess = (event) => {
      this.db = event.target.result;
    };
  }

  async saveBeer(beer) {
    if (!this.db) return;
    const transaction = this.db.transaction(['beers'], 'readwrite');
    const store = transaction.objectStore('beers');
    await store.put(beer);
  }

  async getBeers() {
    if (!this.db) return [];
    const transaction = this.db.transaction(['beers'], 'readonly');
    const store = transaction.objectStore('beers');
    return new Promise((resolve) => {
      const request = store.getAll();
      request.onsuccess = () => resolve(request.result);
    });
  }
}

export const offlineManager = new OfflineManager();

Caching Strategies

Implement different caching strategies for different resource types:

JavaScript
// Cache-first strategy for static assets
const cacheFirst = async (request) => {
  const cachedResponse = await caches.match(request);
  if (cachedResponse) {
    return cachedResponse;
  }
  return fetch(request);
};

// Network-first strategy for dynamic content
const networkFirst = async (request) => {
  try {
    const networkResponse = await fetch(request);
    if (networkResponse.ok) {
      const cache = await caches.open(CACHE_NAME);
      cache.put(request, networkResponse.clone());
      return networkResponse;
    }
  } catch (error) {
    const cachedResponse = await caches.match(request);
    if (cachedResponse) {
      return cachedResponse;
    }
    throw error;
  }
};

// Stale-while-revalidate strategy
const staleWhileRevalidate = async (request) => {
  const cache = await caches.open(CACHE_NAME);
  const cachedResponse = await cache.match(request);
  
  const fetchPromise = fetch(request).then((networkResponse) => {
    cache.put(request, networkResponse.clone());
    return networkResponse;
  });
  
  return cachedResponse || fetchPromise;
};

Install Prompt

Handle PWA installation prompts:

Svelte
<!-- src/components/pwa/InstallPrompt.svelte -->
<script>
  import { onMount } from 'svelte';
  import { browser } from '$app/environment';

  let deferredPrompt;
  let showInstallButton = false;

  onMount(() => {
    if (!browser) return;

    window.addEventListener('beforeinstallprompt', (e) => {
      e.preventDefault();
      deferredPrompt = e;
      showInstallButton = true;
    });

    window.addEventListener('appinstalled', () => {
      showInstallButton = false;
      deferredPrompt = null;
    });
  });

  async function installApp() {
    if (!deferredPrompt) return;
    
    deferredPrompt.prompt();
    const { outcome } = await deferredPrompt.userChoice;
    
    if (outcome === 'accepted') {
      console.log('User accepted the install prompt');
    }
    
    deferredPrompt = null;
    showInstallButton = false;
  }
</script>

{#if showInstallButton}
  <button on:click={installApp} class="install-button">
    Install BrewHoard
  </button>
{/if}

<style>
  .install-button {
    position: fixed;
    bottom: 20px;
    right: 20px;
    background: #1f2937;
    color: white;
    border: none;
    padding: 12px 24px;
    border-radius: 8px;
    cursor: pointer;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  }
</style>

Push Notifications

Set up push notifications for updates and reminders:

JavaScript
// src/lib/pwa/push-manager.js
import { browser } from '$app/environment';

class PushManager {
  constructor() {
    if (browser) {
      this.registration = null;
      this.init();
    }
  }

  async init() {
    if ('serviceWorker' in navigator) {
      this.registration = await navigator.serviceWorker.ready;
    }
  }

  async requestPermission() {
    if (!('Notification' in window)) return false;
    
    const permission = await Notification.requestPermission();
    return permission === 'granted';
  }

  async subscribe() {
    if (!this.registration) return null;
    
    const response = await fetch('/api/push/subscribe', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        subscription: await this.registration.pushManager.subscribe({
          userVisibleOnly: true,
          applicationServerKey: this.urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
        })
      })
    });
    
    return response.json();
  }

  urlBase64ToUint8Array(base64String) {
    const padding = '='.repeat((4 - base64String.length % 4) % 4);
    const base64 = (base64String + padding)
      .replace(/-/g, '+')
      .replace(/_/g, '/');
    
    const rawData = window.atob(base64);
    const outputArray = new Uint8Array(rawData.length);
    
    for (let i = 0; i < rawData.length; ++i) {
      outputArray[i] = rawData.charCodeAt(i);
    }
    return outputArray;
  }
}

export const pushManager = new PushManager();

App Icons

Generate and configure app icons for different sizes:

  1. Create icons in these sizes: 192x192, 512x512
  2. Include both regular and maskable versions
  3. Place in static/ directory
  4. Reference in manifest.json

Example icon generation script:

Bash
# Using ImageMagick
convert source-icon.png -resize 192x192 static/pwa-192x192.png
convert source-icon.png -resize 512x512 static/pwa-512x512.png

# For maskable icons (with padding)
convert source-icon.png -resize 144x144 -background transparent -gravity center -extent 192x192 static/pwa-192x192-maskable.png

Next Steps