diff --git a/.kiro/specs/ultimate-lists-app/tasks.md b/.kiro/specs/ultimate-lists-app/tasks.md
index 278e7ad..9aaa2fb 100644
--- a/.kiro/specs/ultimate-lists-app/tasks.md
+++ b/.kiro/specs/ultimate-lists-app/tasks.md
@@ -151,7 +151,7 @@
- [x] 10. Checkpoint - Ensure all tests pass
- Ensure all tests pass, ask the user if questions arise.
-- [ ] 11. Set up PWA infrastructure
+- [x] 11. Set up PWA infrastructure
- Create service worker (public/sw.js) with caching strategies
- Implement cache-first strategy for static assets
- Implement stale-while-revalidate for API responses
diff --git a/app/layout.tsx b/app/layout.tsx
index ec6c5a8..47f9146 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,12 +1,23 @@
import type { Metadata } from 'next'
import './globals.css'
+import ServiceWorkerRegistration from '@/components/ServiceWorkerRegistration'
export const metadata: Metadata = {
title: 'Ultimate Lists',
- description: 'The ultimate todo list app',
+ description: 'The ultimate todo list app with advanced task management',
manifest: '/manifest.json',
themeColor: '#3B82F6',
viewport: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no',
+ appleWebApp: {
+ capable: true,
+ statusBarStyle: 'default',
+ title: 'Ultimate Lists',
+ },
+ applicationName: 'Ultimate Lists',
+ icons: {
+ icon: '/icon-192.svg',
+ apple: '/icon-192.svg',
+ },
}
export default function RootLayout({
@@ -16,7 +27,17 @@ export default function RootLayout({
}) {
return (
-
{children}
+
+
+
+
+
+
+
+
+
+ {children}
+
)
}
\ No newline at end of file
diff --git a/app/offline/page.tsx b/app/offline/page.tsx
new file mode 100644
index 0000000..3ee02f4
--- /dev/null
+++ b/app/offline/page.tsx
@@ -0,0 +1,50 @@
+'use client';
+
+import { WifiOff } from 'lucide-react';
+
+export default function OfflinePage() {
+ return (
+
+
+
+
+
+ You're Offline
+
+
+
+ It looks like you've lost your internet connection. Don't worry, your data is safe and will sync when you're back online.
+
+
+
+
What you can do:
+
+ -
+ •
+ View your cached lists and tasks
+
+ -
+ •
+ Make changes that will sync later
+
+ -
+ •
+ Check your connection and try again
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/ServiceWorkerRegistration.tsx b/components/ServiceWorkerRegistration.tsx
new file mode 100644
index 0000000..6006cc4
--- /dev/null
+++ b/components/ServiceWorkerRegistration.tsx
@@ -0,0 +1,44 @@
+'use client';
+
+import { useEffect } from 'react';
+
+export default function ServiceWorkerRegistration() {
+ useEffect(() => {
+ if ('serviceWorker' in navigator) {
+ // Register service worker
+ navigator.serviceWorker
+ .register('/sw.js')
+ .then((registration) => {
+ console.log('Service Worker registered with scope:', registration.scope);
+
+ // Check for updates periodically
+ setInterval(() => {
+ registration.update();
+ }, 60000); // Check every minute
+
+ // Listen for updates
+ registration.addEventListener('updatefound', () => {
+ const newWorker = registration.installing;
+ if (newWorker) {
+ newWorker.addEventListener('statechange', () => {
+ if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
+ // New service worker available, could show update notification
+ console.log('New service worker available');
+ }
+ });
+ }
+ });
+ })
+ .catch((error) => {
+ console.error('Service Worker registration failed:', error);
+ });
+
+ // Listen for controller change (new service worker activated)
+ navigator.serviceWorker.addEventListener('controllerchange', () => {
+ console.log('Service Worker controller changed');
+ });
+ }
+ }, []);
+
+ return null; // This component doesn't render anything
+}
diff --git a/public/icon-192.svg b/public/icon-192.svg
new file mode 100644
index 0000000..ac6a5ca
--- /dev/null
+++ b/public/icon-192.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/public/icon-512.svg b/public/icon-512.svg
new file mode 100644
index 0000000..14f6b9f
--- /dev/null
+++ b/public/icon-512.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/public/icon.svg b/public/icon.svg
new file mode 100644
index 0000000..32dddb2
--- /dev/null
+++ b/public/icon.svg
@@ -0,0 +1,6 @@
+
diff --git a/public/manifest.json b/public/manifest.json
index 1c2bf33..421f902 100644
--- a/public/manifest.json
+++ b/public/manifest.json
@@ -1,21 +1,37 @@
{
"name": "Ultimate Lists",
"short_name": "Lists",
- "description": "The ultimate todo list app",
+ "description": "The ultimate todo list app with advanced task management",
"start_url": "/",
+ "scope": "/",
"display": "standalone",
+ "orientation": "portrait-primary",
"background_color": "#ffffff",
"theme_color": "#3B82F6",
+ "categories": ["productivity", "utilities"],
"icons": [
{
- "src": "/icon-192.png",
+ "src": "/icon-192.svg",
"sizes": "192x192",
- "type": "image/png"
+ "type": "image/svg+xml",
+ "purpose": "any maskable"
},
{
- "src": "/icon-512.png",
+ "src": "/icon-512.svg",
"sizes": "512x512",
- "type": "image/png"
+ "type": "image/svg+xml",
+ "purpose": "any maskable"
}
- ]
+ ],
+ "screenshots": [],
+ "shortcuts": [
+ {
+ "name": "New List",
+ "short_name": "New List",
+ "description": "Create a new list",
+ "url": "/?action=new-list",
+ "icons": []
+ }
+ ],
+ "prefer_related_applications": false
}
\ No newline at end of file
diff --git a/public/sw.js b/public/sw.js
new file mode 100644
index 0000000..9b2850a
--- /dev/null
+++ b/public/sw.js
@@ -0,0 +1,164 @@
+// Service Worker for Ultimate Lists App
+// Implements caching strategies for offline functionality
+
+const CACHE_NAME = 'ultimate-lists-v1';
+const RUNTIME_CACHE = 'ultimate-lists-runtime-v1';
+
+// Static assets to cache on install
+const STATIC_ASSETS = [
+ '/',
+ '/offline',
+ '/manifest.json',
+ '/icon-192.svg',
+ '/icon-512.svg',
+];
+
+// Install event - cache static assets
+self.addEventListener('install', (event) => {
+ event.waitUntil(
+ caches.open(CACHE_NAME).then((cache) => {
+ return cache.addAll(STATIC_ASSETS).catch((error) => {
+ console.error('Failed to cache static assets:', error);
+ // Continue even if some assets fail to cache
+ return Promise.resolve();
+ });
+ })
+ );
+ // Activate immediately
+ 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 && name !== RUNTIME_CACHE)
+ .map((name) => caches.delete(name))
+ );
+ })
+ );
+ // Take control of all pages immediately
+ return self.clients.claim();
+});
+
+// Fetch event - implement caching strategies
+self.addEventListener('fetch', (event) => {
+ const { request } = event;
+ const url = new URL(request.url);
+
+ // Skip non-GET requests
+ if (request.method !== 'GET') {
+ return;
+ }
+
+ // Skip chrome extensions and other protocols
+ if (!url.protocol.startsWith('http')) {
+ return;
+ }
+
+ // Cache-first strategy for static assets
+ if (isStaticAsset(url)) {
+ event.respondWith(cacheFirst(request));
+ return;
+ }
+
+ // Stale-while-revalidate for API responses and pages
+ if (isApiRequest(url) || isPageRequest(url)) {
+ event.respondWith(staleWhileRevalidate(request));
+ return;
+ }
+
+ // Network-first for everything else
+ event.respondWith(networkFirst(request));
+});
+
+// Helper: Check if request is for a static asset
+function isStaticAsset(url) {
+ const staticExtensions = ['.js', '.css', '.png', '.jpg', '.jpeg', '.svg', '.ico', '.woff', '.woff2'];
+ return staticExtensions.some((ext) => url.pathname.endsWith(ext));
+}
+
+// Helper: Check if request is an API request
+function isApiRequest(url) {
+ return url.pathname.startsWith('/api/');
+}
+
+// Helper: Check if request is a page request
+function isPageRequest(url) {
+ return url.origin === self.location.origin && !url.pathname.includes('.');
+}
+
+// Strategy: Cache-first (for static assets)
+async function cacheFirst(request) {
+ const cache = await caches.open(CACHE_NAME);
+ const cached = await cache.match(request);
+
+ if (cached) {
+ return cached;
+ }
+
+ try {
+ const response = await fetch(request);
+ if (response.ok) {
+ cache.put(request, response.clone());
+ }
+ return response;
+ } catch (error) {
+ console.error('Cache-first fetch failed:', error);
+ return new Response('Network error', { status: 408 });
+ }
+}
+
+// Strategy: Stale-while-revalidate (for API and pages)
+async function staleWhileRevalidate(request) {
+ const cache = await caches.open(RUNTIME_CACHE);
+ const cached = await cache.match(request);
+
+ const fetchPromise = fetch(request).then((response) => {
+ if (response.ok) {
+ cache.put(request, response.clone());
+ }
+ return response;
+ }).catch((error) => {
+ console.error('Stale-while-revalidate fetch failed:', error);
+ // If we have a cached version, return it on network error
+ if (cached) {
+ return cached;
+ }
+ // Otherwise, return offline page for navigation requests
+ if (request.mode === 'navigate') {
+ return caches.match('/offline');
+ }
+ throw error;
+ });
+
+ return cached || fetchPromise;
+}
+
+// Strategy: Network-first (fallback)
+async function networkFirst(request) {
+ try {
+ const response = await fetch(request);
+ if (response.ok) {
+ const cache = await caches.open(RUNTIME_CACHE);
+ cache.put(request, response.clone());
+ }
+ return response;
+ } catch (error) {
+ const cache = await caches.open(RUNTIME_CACHE);
+ const cached = await cache.match(request);
+
+ if (cached) {
+ return cached;
+ }
+
+ // Return offline page for navigation requests
+ if (request.mode === 'navigate') {
+ return caches.match('/offline');
+ }
+
+ throw error;
+ }
+}
diff --git a/scripts/generate-icons.js b/scripts/generate-icons.js
new file mode 100644
index 0000000..1833f1b
--- /dev/null
+++ b/scripts/generate-icons.js
@@ -0,0 +1,45 @@
+// Icon generation script
+// This script creates placeholder icon files for the PWA
+// In production, you should replace these with proper high-quality icons
+
+const fs = require('fs');
+const path = require('path');
+
+// Create a simple data URL for a blue icon with a checkmark
+function createIconDataURL(size) {
+ // Simple SVG icon
+ const svg = `
+
+ `;
+
+ return `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}`;
+}
+
+// For now, we'll create SVG files that browsers can use
+function createSVGIcon(size, outputPath) {
+ const svg = ``;
+
+ fs.writeFileSync(outputPath, svg);
+ console.log(`Created ${outputPath}`);
+}
+
+// Create icons
+const publicDir = path.join(__dirname, '..', 'public');
+
+createSVGIcon(192, path.join(publicDir, 'icon-192.svg'));
+createSVGIcon(512, path.join(publicDir, 'icon-512.svg'));
+
+console.log('\nNote: SVG icons created. For production, convert these to PNG using:');
+console.log(' - Online tools like https://cloudconvert.com/svg-to-png');
+console.log(' - ImageMagick: convert icon-192.svg icon-192.png');
+console.log(' - Or use a design tool like Figma/Sketch to export PNG versions');