Progressive Web App Features - dinesh-git17/my-progress-planner GitHub Wiki

Comprehensive guide to My Progress Planner's PWA capabilities, offline functionality, and native app-like experience.

๐Ÿ“ฑ PWA Overview

My Progress Planner is built as a Progressive Web App, providing a native app experience while remaining accessible through any modern web browser.

Core PWA Benefits

  • ๐Ÿš€ Instant Loading - App shell loads immediately
  • ๐Ÿ“ฑ Installable - Add to home screen like a native app
  • ๐Ÿ”„ Offline-First - Works without internet connection
  • ๐Ÿ”” Push Notifications - Stay engaged with meal reminders
  • ๐Ÿ”„ Background Sync - Data syncs when connection returns
  • ๐Ÿ’พ Reliable - Always works, regardless of network quality

๐Ÿ—๏ธ PWA Architecture

graph TB
    subgraph "Browser Environment"
        A[Web App] --> B[Service Worker]
        B --> C[Cache API]
        B --> D[IndexedDB]
        B --> E[Background Sync]
        B --> F[Push Notifications]
    end
    
    subgraph "Caching Layers"
        C --> G[App Shell Cache]
        C --> H[API Cache]
        C --> I[Image Cache]
        C --> J[Dynamic Cache]
    end
    
    subgraph "Offline Storage"
        D --> K[User Data]
        D --> L[Pending Syncs]
        D --> M[Cached Responses]
    end
    
    A --> N[Network]
    N --> O[API Server]
    N --> P[CDN Assets]
Loading

๐Ÿ“ฆ Installation Experience

Web App Manifest

{
  "name": "My Progress Planner",
  "short_name": "Progress",
  "description": "Track your meals with love and support",
  "start_url": "/",
  "display": "standalone",
  "display_override": ["fullscreen", "standalone"],
  "background_color": "#f5ede6",
  "theme_color": "#f5ede6",
  "orientation": "portrait-primary",
  "categories": ["health", "lifestyle", "productivity"],
  "shortcuts": [
    {
      "name": "Log Breakfast",
      "url": "/breakfast",
      "icons": [{"src": "/icon-192.png", "sizes": "192x192"}]
    },
    {
      "name": "Log Lunch",
      "url": "/lunch",
      "icons": [{"src": "/icon-192.png", "sizes": "192x192"}]
    },
    {
      "name": "Log Dinner",
      "url": "/dinner",
      "icons": [{"src": "/icon-192.png", "sizes": "192x192"}]
    }
  ]
}

Installation Prompts

interface InstallPrompt {
  platform: 'ios' | 'android' | 'desktop';
  trigger: 'automatic' | 'manual' | 'engagement-based';
  timing: 'immediate' | 'after-usage' | 'on-return-visit';
}

class PWAInstallManager {
  private deferredPrompt: any = null;
  
  constructor() {
    this.setupInstallPromptHandling();
  }
  
  private setupInstallPromptHandling() {
    window.addEventListener('beforeinstallprompt', (e) => {
      e.preventDefault();
      this.deferredPrompt = e;
      this.showInstallButton();
    });
    
    window.addEventListener('appinstalled', () => {
      this.trackInstallation();
      this.hideInstallButton();
    });
  }
  
  async promptInstall(): Promise<boolean> {
    if (!this.deferredPrompt) return false;
    
    this.deferredPrompt.prompt();
    const result = await this.deferredPrompt.userChoice;
    
    if (result.outcome === 'accepted') {
      this.trackInstallation();
      return true;
    }
    
    return false;
  }
}

Cross-Platform Installation

iOS Safari

// iOS-specific install guidance
const IOSInstallPrompt = () => {
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
  const isInStandaloneMode = window.matchMedia('(display-mode: standalone)').matches;
  
  if (isIOS && !isInStandaloneMode) {
    return (
      <div className="ios-install-prompt">
        <p>Install this app on your iPhone:</p>
        <ol>
          <li>Tap the Share button in Safari</li>
          <li>Select "Add to Home Screen"</li>
          <li>Tap "Add" to install</li>
        </ol>
      </div>
    );
  }
  return null;
};

Android Chrome

// Android-specific features
const AndroidFeatures = {
  webApk: true,              // Generate WebAPK
  shortcuts: true,           // App shortcuts
  shareTarget: true,         // Handle shared content
  backgroundSync: true,      // Background data sync
  pushNotifications: true    // Native notifications
};

๐Ÿ”„ Offline-First Architecture

Service Worker Strategy

// Service worker with multiple caching strategies
const CACHE_STRATEGIES = {
  // Static assets - cache first, update in background
  'app-shell': {
    strategy: 'CacheFirst',
    cacheName: 'app-shell-v1',
    resources: ['/', '/manifest.json', '/icons/*']
  },
  
  // API data - network first, fallback to cache
  'api-data': {
    strategy: 'NetworkFirst',
    cacheName: 'api-cache-v1',
    endpoints: ['/api/meals/*', '/api/streak', '/api/summaries/*']
  },
  
  // Images - cache first with expiration
  'images': {
    strategy: 'CacheFirst',
    cacheName: 'image-cache-v1',
    expiration: { maxAgeSeconds: 30 * 24 * 60 * 60 } // 30 days
  },
  
  // AI quotes - stale while revalidate
  'ai-quotes': {
    strategy: 'StaleWhileRevalidate',
    cacheName: 'ai-cache-v1',
    endpoints: ['/api/ai/quote']
  }
};

Background Sync Implementation

interface PendingSync {
  id: string;
  type: 'meal-log' | 'profile-update' | 'friend-add';
  data: any;
  timestamp: Date;
  retries: number;
  maxRetries: number;
}

class BackgroundSyncManager {
  private readonly SYNC_TAG = 'progress-planner-sync';
  private readonly DB_NAME = 'offline-queue';
  
  async queueSync(syncData: Omit<PendingSync, 'id' | 'retries'>): Promise<void> {
    const pendingSync: PendingSync = {
      id: crypto.randomUUID(),
      retries: 0,
      maxRetries: 3,
      ...syncData
    };
    
    // Store in IndexedDB
    await this.storePendingSync(pendingSync);
    
    // Register background sync
    if ('serviceWorker' in navigator && 'sync' in window.ServiceWorkerRegistration.prototype) {
      const registration = await navigator.serviceWorker.ready;
      await registration.sync.register(this.SYNC_TAG);
    } else {
      // Fallback: Retry immediately if online
      if (navigator.onLine) {
        await this.processPendingSync(pendingSync);
      }
    }
  }
  
  async processPendingSyncs(): Promise<void> {
    const pendingSyncs = await this.getPendingSyncs();
    
    for (const sync of pendingSyncs) {
      try {
        await this.processPendingSync(sync);
        await this.removePendingSync(sync.id);
      } catch (error) {
        await this.handleSyncError(sync, error);
      }
    }
  }
  
  private async processPendingSync(sync: PendingSync): Promise<void> {
    switch (sync.type) {
      case 'meal-log':
        await this.syncMealLog(sync.data);
        break;
      case 'profile-update':
        await this.syncProfileUpdate(sync.data);
        break;
      case 'friend-add':
        await this.syncFriendAdd(sync.data);
        break;
    }
  }
}

Offline Data Storage

interface OfflineStorage {
  meals: Map<string, Meal>;
  userProfile: UserProfile | null;
  streakData: StreakData | null;
  cachedResponses: Map<string, CachedResponse>;
  lastSyncTime: Date | null;
}

class OfflineStorageManager {
  private db: IDBDatabase | null = null;
  private readonly DB_NAME = 'progress-planner-offline';
  private readonly DB_VERSION = 1;
  
  async initialize(): Promise<void> {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.DB_NAME, this.DB_VERSION);
      
      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve();
      };
      
      request.onupgradeneeded = (event) => {
        const db = (event.target as IDBOpenDBRequest).result;
        
        // Create object stores
        if (!db.objectStoreNames.contains('meals')) {
          const mealStore = db.createObjectStore('meals', { keyPath: 'id' });
          mealStore.createIndex('date', 'logged_at');
          mealStore.createIndex('type', 'meal_type');
        }
        
        if (!db.objectStoreNames.contains('sync-queue')) {
          db.createObjectStore('sync-queue', { keyPath: 'id' });
        }
        
        if (!db.objectStoreNames.contains('user-data')) {
          db.createObjectStore('user-data', { keyPath: 'key' });
        }
      };
    });
  }
  
  async storeMeal(meal: Meal): Promise<void> {
    const transaction = this.db!.transaction(['meals'], 'readwrite');
    const store = transaction.objectStore('meals');
    await store.put(meal);
  }
  
  async getMealsForDate(date: string): Promise<Meal[]> {
    const transaction = this.db!.transaction(['meals'], 'readonly');
    const store = transaction.objectStore('meals');
    const index = store.index('date');
    const meals = await index.getAll(IDBKeyRange.bound(
      new Date(date + 'T00:00:00'),
      new Date(date + 'T23:59:59')
    ));
    return meals;
  }
}

๐Ÿ”” Push Notifications

Notification Setup

interface NotificationConfig {
  enabled: boolean;
  mealReminders: boolean;
  streakCelebrations: boolean;
  dailySummaries: boolean;
  friendActivity: boolean;
}

class PushNotificationManager {
  private registration: ServiceWorkerRegistration | null = null;
  
  async requestPermission(): Promise<NotificationPermission> {
    if (!('Notification' in window)) {
      throw new Error('Notifications not supported');
    }
    
    const permission = await Notification.requestPermission();
    
    if (permission === 'granted') {
      await this.subscribeToNotifications();
    }
    
    return permission;
  }
  
  private async subscribeToNotifications(): Promise<void> {
    this.registration = await navigator.serviceWorker.ready;
    
    const subscription = await this.registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: this.urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
    });
    
    // Send subscription to server
    await fetch('/api/notifications/subscribe', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ subscription })
    });
  }
  
  async scheduleReminder(mealType: string, time: string): Promise<void> {
    await fetch('/api/notifications/schedule', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        type: 'meal-reminder',
        mealType,
        scheduledTime: time
      })
    });
  }
}

Notification Types

interface NotificationData {
  type: 'meal-reminder' | 'streak-milestone' | 'daily-summary' | 'encouragement';
  title: string;
  body: string;
  icon: string;
  badge: string;
  actions?: NotificationAction[];
  data?: any;
}

const NOTIFICATION_TEMPLATES = {
  'meal-reminder': {
    breakfast: {
      title: "Good morning! ๐ŸŒ…",
      body: "Ready to start your day with some nourishing fuel?",
      actions: [
        { action: 'log-meal', title: 'Log Breakfast' },
        { action: 'remind-later', title: 'Remind me later' }
      ]
    },
    lunch: {
      title: "Lunch time! ๐Ÿฝ๏ธ",
      body: "Time to refuel and take care of yourself!",
      actions: [
        { action: 'log-meal', title: 'Log Lunch' },
        { action: 'dismiss', title: 'Not now' }
      ]
    },
    dinner: {
      title: "Dinner time! ๐ŸŒ™",
      body: "End your day with some delicious nourishment!",
      actions: [
        { action: 'log-meal', title: 'Log Dinner' },
        { action: 'view-summary', title: 'View today' }
      ]
    }
  },
  
  'streak-milestone': {
    title: "Amazing streak! ๐Ÿ”ฅ",
    body: "You've logged meals for {streak} days straight! So proud of you!",
    actions: [
      { action: 'celebrate', title: 'Celebrate!' },
      { action: 'share', title: 'Share progress' }
    ]
  }
};

๐ŸŽจ Native App Experience

App Shell Architecture

// App shell provides instant loading
const AppShell = () => {
  return (
    <div className="app-shell">
      <header className="app-header">
        <nav className="navigation">
          <div className="nav-skeleton" />
        </nav>
      </header>
      
      <main className="app-main">
        <div className="content-skeleton">
          <div className="meal-card-skeleton" />
          <div className="meal-card-skeleton" />
          <div className="meal-card-skeleton" />
        </div>
      </main>
      
      <footer className="app-footer">
        <div className="bottom-nav-skeleton" />
      </footer>
    </div>
  );
};

Splash Screens & Icons

// Comprehensive icon set for all platforms
const PWA_ICONS = {
  'icon-48': { size: '48x48', purpose: 'any' },
  'icon-72': { size: '72x72', purpose: 'any' },
  'icon-96': { size: '96x96', purpose: 'any' },
  'icon-144': { size: '144x144', purpose: 'any' },
  'icon-192': { size: '192x192', purpose: 'any' },
  'icon-512': { size: '512x512', purpose: 'any' },
  'icon-maskable-192': { size: '192x192', purpose: 'maskable' },
  'icon-maskable-512': { size: '512x512', purpose: 'maskable' }
};

// iOS splash screens for different devices
const IOS_SPLASH_SCREENS = [
  // iPhone 15 Pro Max, 15 Plus, 14 Pro Max
  {
    media: '(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3)',
    href: '/splash/iphone-15-pro-max.png'
  },
  // iPhone 15 Pro, 15, 14 Pro
  {
    media: '(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3)',
    href: '/splash/iphone-15-pro.png'
  },
  // iPad Pro 12.9"
  {
    media: '(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2)',
    href: '/splash/ipad-pro-12.9.png'
  }
];

Native-Like Interactions

// Touch gestures and native feel
class NativeInteractions {
  private setupTouchHandlers() {
    // Pull-to-refresh
    let startY = 0;
    let isRefreshing = false;
    
    document.addEventListener('touchstart', (e) => {
      startY = e.touches[0].clientY;
    });
    
    document.addEventListener('touchmove', (e) => {
      if (window.scrollY === 0 && !isRefreshing) {
        const currentY = e.touches[0].clientY;
        const pullDistance = currentY - startY;
        
        if (pullDistance > 100) {
          this.showRefreshIndicator();
        }
      }
    });
    
    document.addEventListener('touchend', async () => {
      if (this.shouldRefresh()) {
        isRefreshing = true;
        await this.refreshData();
        this.hideRefreshIndicator();
        isRefreshing = false;
      }
    });
  }
  
  private setupSwipeGestures() {
    // Swipe between meal types
    let startX = 0;
    let startTime = 0;
    
    document.addEventListener('touchstart', (e) => {
      startX = e.touches[0].clientX;
      startTime = Date.now();
    });
    
    document.addEventListener('touchend', (e) => {
      const endX = e.changedTouches[0].clientX;
      const endTime = Date.now();
      const distance = Math.abs(endX - startX);
      const duration = endTime - startTime;
      
      // Fast swipe detection
      if (distance > 50 && duration < 300) {
        const direction = endX > startX ? 'right' : 'left';
        this.handleSwipe(direction);
      }
    });
  }
}

๐Ÿ”ง Performance Optimization

Lazy Loading & Code Splitting

// Route-based code splitting
const LazyRoutes = {
  Dashboard: lazy(() => import('../pages/Dashboard')),
  MealLog: lazy(() => import('../pages/MealLog')),
  Summaries: lazy(() => import('../pages/Summaries')),
  Profile: lazy(() => import('../pages/Profile'))
};

// Component-based lazy loading
const LazyComponents = {
  AIResponseCard: lazy(() => import('../components/AIResponseCard')),
  StreakVisualizer: lazy(() => import('../components/StreakVisualizer')),
  FriendsManager: lazy(() => import('../components/FriendsManager'))
};

Resource Optimization

// Preload critical resources
class ResourceOptimizer {
  preloadCriticalResources() {
    // Preload essential fonts
    const fontLink = document.createElement('link');
    fontLink.rel = 'preload';
    fontLink.href = '/fonts/dm-sans-400.woff2';
    fontLink.as = 'font';
    fontLink.type = 'font/woff2';
    fontLink.crossOrigin = 'anonymous';
    document.head.appendChild(fontLink);
    
    // Preload critical images
    const heroImage = new Image();
    heroImage.src = '/images/hero-meal-tracking.webp';
    
    // Prefetch likely next pages
    this.prefetchRoutes(['/breakfast', '/lunch', '/dinner']);
  }
  
  private prefetchRoutes(routes: string[]) {
    routes.forEach(route => {
      const link = document.createElement('link');
      link.rel = 'prefetch';
      link.href = route;
      document.head.appendChild(link);
    });
  }
}

Bundle Optimization

// Next.js bundle analysis
const BundleAnalyzer = require('@next/bundle-analyzer');

const withBundleAnalyzer = BundleAnalyzer({
  enabled: process.env.ANALYZE === 'true'
});

const nextConfig = {
  // Optimize chunks
  webpack: (config) => {
    config.optimization.splitChunks = {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
        common: {
          minChunks: 2,
          priority: -10,
          reuseExistingChunk: true,
        },
      },
    };
    
    return config;
  }
};

module.exports = withBundleAnalyzer(nextConfig);

๐Ÿ“Š PWA Analytics & Monitoring

Performance Metrics

interface PWAMetrics {
  installRate: number;
  retentionRate: number;
  offlineUsage: number;
  syncSuccessRate: number;
  notificationEngagement: number;
  loadTimes: {
    firstContentfulPaint: number;
    largestContentfulPaint: number;
    timeToInteractive: number;
  };
}

class PWAAnalytics {
  private metrics: PWAMetrics = {
    installRate: 0,
    retentionRate: 0,
    offlineUsage: 0,
    syncSuccessRate: 0,
    notificationEngagement: 0,
    loadTimes: {
      firstContentfulPaint: 0,
      largestContentfulPaint: 0,
      timeToInteractive: 0
    }
  };
  
  trackInstall() {
    this.sendEvent('pwa_install', {
      timestamp: new Date(),
      platform: this.detectPlatform(),
      userAgent: navigator.userAgent
    });
  }
  
  trackOfflineUsage() {
    if (!navigator.onLine) {
      this.sendEvent('offline_usage', {
        timestamp: new Date(),
        action: 'meal_logged_offline'
      });
    }
  }
  
  private sendEvent(eventName: string, data: any) {
    // Send to analytics service
    fetch('/api/analytics/track', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ event: eventName, data })
    }).catch(() => {
      // Store in IndexedDB for later sync
      this.queueAnalyticsEvent(eventName, data);
    });
  }
}

User Engagement Tracking

interface EngagementMetrics {
  sessionDuration: number;
  pagesPerSession: number;
  bounceRate: number;
  returnVisitorRate: number;
  featureUsage: Record<string, number>;
}

class EngagementTracker {
  private sessionStart: Date;
  private pageViews: number = 0;
  
  constructor() {
    this.sessionStart = new Date();
    this.setupTracking();
  }
  
  private setupTracking() {
    // Page visibility changes
    document.addEventListener('visibilitychange', () => {
      if (document.hidden) {
        this.trackSessionEnd();
      } else {
        this.trackSessionStart();
      }
    });
    
    // Route changes
    this.trackPageView();
    
    // Feature usage
    this.trackFeatureUsage();
  }
  
  private trackFeatureUsage() {
    // Track meal logging
    document.addEventListener('meal-logged', (e) => {
      this.trackEvent('meal_logged', {
        mealType: (e as CustomEvent).detail.mealType,
        offline: !navigator.onLine
      });
    });
    
    // Track AI interactions
    document.addEventListener('ai-response-received', () => {
      this.trackEvent('ai_interaction');
    });
    
    // Track PWA features
    document.addEventListener('notification-clicked', () => {
      this.trackEvent('notification_engagement');
    });
  }
}

๐Ÿงช Testing PWA Features

PWA Testing Checklist

interface PWATestSuite {
  manifest: {
    validManifest: boolean;
    installable: boolean;
    iconsPresent: boolean;
    splashScreens: boolean;
  };
  serviceWorker: {
    registered: boolean;
    cachingWorking: boolean;
    offlineSupport: boolean;
    backgroundSync: boolean;
  };
  performance: {
    lighthouse: number;
    loadTime: number;
    interactivity: number;
  };
  notifications: {
    permission: boolean;
    delivery: boolean;
    actions: boolean;
  };
}

class PWATester {
  async runFullTest(): Promise<PWATestSuite> {
    return {
      manifest: await this.testManifest(),
      serviceWorker: await this.testServiceWorker(),
      performance: await this.testPerformance(),
      notifications: await this.testNotifications()
    };
  }
  
  private async testManifest(): Promise<PWATestSuite['manifest']> {
    const manifestResponse = await fetch('/manifest.json');
    const manifest = await manifestResponse.json();
    
    return {
      validManifest: this.validateManifest(manifest),
      installable: await this.checkInstallability(),
      iconsPresent: manifest.icons && manifest.icons.length > 0,
      splashScreens: this.checkSplashScreens(manifest)
    };
  }
  
  private async testServiceWorker(): Promise<PWATestSuite['serviceWorker']> {
    const registration = await navigator.serviceWorker.getRegistration();
    
    return {
      registered: !!registration,
      cachingWorking: await this.testCaching(),
      offlineSupport: await this.testOfflineSupport(),
      backgroundSync: 'sync' in ServiceWorkerRegistration.prototype
    };
  }
}

Automated PWA Testing

// Playwright PWA testing
const { test, expect } = require('@playwright/test');

test.describe('PWA Features', () => {
  test('should be installable', async ({ page }) => {
    await page.goto('/');
    
    // Check manifest
    const manifestResponse = await page.request.get('/manifest.json');
    expect(manifestResponse.ok()).toBeTruthy();
    
    const manifest = await manifestResponse.json();
    expect(manifest.name).toBe('My Progress Planner');
    expect(manifest.display).toBe('standalone');
  });
  
  test('should work offline', async ({ page, context }) => {
    await page.goto('/');
    await page.waitForLoadState('networkidle');
    
    // Go offline
    await context.setOffline(true);
    
    // Try to log a meal
    await page.click('[data-testid="breakfast-card"]');
    await page.fill('[data-testid="meal-input"]', 'Oatmeal');
    await page.click('[data-testid="log-meal-button"]');
    
    // Should show offline indicator but allow logging
    await expect(page.locator('[data-testid="offline-indicator"]')).toBeVisible();
    await expect(page.locator('[data-testid="meal-logged-message"]')).toBeVisible();
  });
  
  test('should cache resources', async ({ page }) => {
    await page.goto('/');
    
    // Check if service worker is registered
    const swRegistered = await page.evaluate(() => {
      return 'serviceWorker' in navigator;
    });
    expect(swRegistered).toBeTruthy();
    
    // Check cache contents
    const cacheNames = await page.evaluate(async () => {
      return await caches.keys();
    });
    expect(cacheNames.length).toBeGreaterThan(0);
  });
});

๐Ÿ”ง PWA Development Tools

Debugging PWA Features

class PWADebugger {
  logCacheStatus() {
    caches.keys().then(cacheNames => {
      console.log('Available caches:', cacheNames);
      
      cacheNames.forEach(cacheName => {
        caches.open(cacheName).then(cache => {
          cache.keys().then(keys => {
            console.log(`${cacheName} contains:`, keys.map(k => k.url));
          });
        });
      });
    });
  }
  
  logServiceWorkerStatus() {
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.getRegistrations().then(registrations => {
        console.log('Service Worker registrations:', registrations);
        
        registrations.forEach(registration => {
          console.log('SW state:', registration.active?.state);
          console.log('SW scope:', registration.scope);
        });
      });
    }
  }
  
  testOfflineCapability() {
    const originalOnline = navigator.onLine;
    
    // Simulate offline
    Object.defineProperty(navigator, 'onLine', {
      value: false,
      writable: true
    });
    
    console.log('Testing offline capability...');
    
    // Test key functionality
    this.testMealLogging();
    this.testDataAccess();
    
    // Restore online status
    Object.defineProperty(navigator, 'onLine', {
      value: originalOnline,
      writable: true
    });
  }
}

PWA Build Configuration

// next.config.mjs - PWA optimization
const withPWA = require('next-pwa')({
  dest: 'public',
  disable: process.env.NODE_ENV === 'development',
  register: true,
  skipWaiting: true,
  runtimeCaching: [
    {
      urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
      handler: 'CacheFirst',
      options: {
        cacheName: 'google-fonts',
        expiration: {
          maxEntries: 4,
          maxAgeSeconds: 365 * 24 * 60 * 60 // 1 year
        }
      }
    },
    {
      urlPattern: /\/api\/meals\/.*/i,
      handler: 'NetworkFirst',
      options: {
        cacheName: 'api-cache',
        networkTimeoutSeconds: 10,
        expiration: {
          maxEntries: 100,
          maxAgeSeconds: 24 * 60 * 60 // 1 day
        }
      }
    }
  ]
});

module.exports = withPWA({
  // Other Next.js config
});

๐Ÿ“ž PWA Support

For questions about PWA features, offline functionality, or installation issues:


Our PWA implementation ensures My Progress Planner works seamlessly across all devices and network conditions, providing a native app experience while maintaining the accessibility of the web. ๐Ÿ“ฑ๐Ÿ’•

โš ๏ธ **GitHub.com Fallback** โš ๏ธ