Files
miracles_in_motion/public/sw.js
defiQUG 0b81bcb4f5 feat: Add ProgramsSection component for comprehensive student support
- Implemented ProgramsSection with various support programs including School Supplies, Clothing Support, Emergency Assistance, Educational Technology, Mentorship Programs, and Family Support Services.
- Integrated framer-motion for animations and transitions.
- Added a call-to-action button for requesting program support.

test: Create unit tests for HeroSection component

- Developed tests for rendering, accessibility, and functionality of the HeroSection component using Vitest and Testing Library.
- Mocked framer-motion for testing purposes.

refactor: Update sections index file to include ProgramsSection

- Modified index.tsx to export ProgramsSection alongside existing sections.

feat: Implement LazyImage component for optimized image loading

- Created LazyImage component with lazy loading, error handling, and blur placeholder support.
- Utilized framer-motion for loading animations.

feat: Add PerformanceMonitor component for real-time performance metrics

- Developed PerformanceMonitor to display web vitals and bundle performance metrics.
- Included toggle functionality for development mode.

feat: Create usePerformance hook for performance monitoring

- Implemented usePerformance hook to track web vitals such as FCP, LCP, FID, CLS, and TTFB.
- Added useBundlePerformance hook for monitoring bundle size and loading performance.

test: Set up testing utilities and mocks for components

- Established testing utilities for rendering components with context providers.
- Mocked common hooks and framer-motion components for consistent testing.

feat: Introduce bundleAnalyzer utility for analyzing bundle performance

- Created BundleAnalyzer class to analyze bundle size, suggest optimizations, and generate reports.
- Implemented helper functions for Vite integration and performance monitoring.

chore: Configure Vitest for testing environment and coverage

- Set up Vitest configuration with global variables, jsdom environment, and coverage thresholds.
2025-10-05 09:42:05 -07:00

311 lines
8.2 KiB
JavaScript

// Miracles in Motion - Service Worker
// Version 1.0.0
const CACHE_NAME = 'miracles-in-motion-v1'
const STATIC_CACHE = 'static-v1'
const DYNAMIC_CACHE = 'dynamic-v1'
// Assets to cache immediately
const STATIC_ASSETS = [
'/',
'/index.html',
'/favicon.svg',
'/robots.txt',
'/site.webmanifest'
]
// Assets to cache on demand
const CACHE_STRATEGIES = {
// Cache first for static assets
CACHE_FIRST: ['.js', '.css', '.woff', '.woff2', '.ttf', '.eot'],
// Network first for API calls
NETWORK_FIRST: ['/api/', '/analytics/'],
// Stale while revalidate for images
STALE_WHILE_REVALIDATE: ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg']
}
// Install event - cache static assets
self.addEventListener('install', event => {
console.log('Service Worker: Installing...')
event.waitUntil(
caches.open(STATIC_CACHE)
.then(cache => {
console.log('Service Worker: Caching static assets')
return cache.addAll(STATIC_ASSETS)
})
.then(() => {
console.log('Service Worker: Installation complete')
return self.skipWaiting()
})
.catch(error => {
console.error('Service Worker: Installation failed', error)
})
)
})
// Activate event - clean up old caches
self.addEventListener('activate', event => {
console.log('Service Worker: Activating...')
event.waitUntil(
caches.keys()
.then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME && cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE) {
console.log('Service Worker: Deleting old cache', cacheName)
return caches.delete(cacheName)
}
})
)
})
.then(() => {
console.log('Service Worker: Activation complete')
return self.clients.claim()
})
)
})
// Fetch event - handle requests with appropriate caching strategy
self.addEventListener('fetch', event => {
const { request } = event
const url = new URL(request.url)
// Skip non-GET requests
if (request.method !== 'GET') return
// Skip cross-origin requests
if (url.origin !== location.origin) return
event.respondWith(handleFetch(request))
})
// Determine caching strategy based on file type
function getCachingStrategy(url) {
const pathname = new URL(url).pathname.toLowerCase()
// Check for cache-first assets
if (CACHE_STRATEGIES.CACHE_FIRST.some(ext => pathname.includes(ext))) {
return 'cache-first'
}
// Check for network-first assets
if (CACHE_STRATEGIES.NETWORK_FIRST.some(path => pathname.includes(path))) {
return 'network-first'
}
// Check for stale-while-revalidate assets
if (CACHE_STRATEGIES.STALE_WHILE_REVALIDATE.some(ext => pathname.includes(ext))) {
return 'stale-while-revalidate'
}
// Default to network-first
return 'network-first'
}
// Handle fetch requests with appropriate strategy
async function handleFetch(request) {
const strategy = getCachingStrategy(request.url)
switch (strategy) {
case 'cache-first':
return cacheFirst(request)
case 'network-first':
return networkFirst(request)
case 'stale-while-revalidate':
return staleWhileRevalidate(request)
default:
return networkFirst(request)
}
}
// Cache-first strategy
async function cacheFirst(request) {
try {
const cache = await caches.open(STATIC_CACHE)
const cachedResponse = await cache.match(request)
if (cachedResponse) {
return cachedResponse
}
const networkResponse = await fetch(request)
if (networkResponse.ok) {
cache.put(request, networkResponse.clone())
}
return networkResponse
} catch (error) {
console.error('Cache-first strategy failed:', error)
return new Response('Offline content unavailable', { status: 503 })
}
}
// Network-first strategy
async function networkFirst(request) {
try {
const networkResponse = await fetch(request)
if (networkResponse.ok) {
const cache = await caches.open(DYNAMIC_CACHE)
cache.put(request, networkResponse.clone())
}
return networkResponse
} catch (error) {
console.log('Network failed, trying cache:', error)
const cache = await caches.open(DYNAMIC_CACHE)
const cachedResponse = await cache.match(request)
if (cachedResponse) {
return cachedResponse
}
return new Response('Content unavailable offline', {
status: 503,
headers: { 'Content-Type': 'text/plain' }
})
}
}
// Stale-while-revalidate strategy
async function staleWhileRevalidate(request) {
const cache = await caches.open(DYNAMIC_CACHE)
const cachedResponse = await cache.match(request)
const networkUpdate = fetch(request).then(response => {
if (response.ok) {
cache.put(request, response.clone())
}
return response
})
return cachedResponse || networkUpdate
}
// Background sync for offline form submissions
self.addEventListener('sync', event => {
if (event.tag === 'donation-submission') {
event.waitUntil(syncDonations())
}
if (event.tag === 'assistance-request') {
event.waitUntil(syncAssistanceRequests())
}
})
// Sync offline donations
async function syncDonations() {
try {
const donations = await getOfflineData('pending-donations')
for (const donation of donations) {
await fetch('/api/donations', {
method: 'POST',
body: JSON.stringify(donation),
headers: { 'Content-Type': 'application/json' }
})
}
await clearOfflineData('pending-donations')
console.log('Offline donations synced successfully')
} catch (error) {
console.error('Failed to sync donations:', error)
}
}
// Sync offline assistance requests
async function syncAssistanceRequests() {
try {
const requests = await getOfflineData('pending-requests')
for (const request of requests) {
await fetch('/api/assistance-requests', {
method: 'POST',
body: JSON.stringify(request),
headers: { 'Content-Type': 'application/json' }
})
}
await clearOfflineData('pending-requests')
console.log('Offline assistance requests synced successfully')
} catch (error) {
console.error('Failed to sync assistance requests:', error)
}
}
// Helper functions for offline data management
function getOfflineData(key) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MiraclesInMotion', 1)
request.onsuccess = () => {
const db = request.result
const transaction = db.transaction(['offlineData'], 'readonly')
const store = transaction.objectStore('offlineData')
const getRequest = store.get(key)
getRequest.onsuccess = () => {
resolve(getRequest.result?.data || [])
}
getRequest.onerror = () => reject(getRequest.error)
}
request.onerror = () => reject(request.error)
})
}
function clearOfflineData(key) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MiraclesInMotion', 1)
request.onsuccess = () => {
const db = request.result
const transaction = db.transaction(['offlineData'], 'readwrite')
const store = transaction.objectStore('offlineData')
const deleteRequest = store.delete(key)
deleteRequest.onsuccess = () => resolve()
deleteRequest.onerror = () => reject(deleteRequest.error)
}
request.onerror = () => reject(request.error)
})
}
// Push notification handling
self.addEventListener('push', event => {
const options = {
body: event.data?.text() || 'New update available',
icon: '/favicon.svg',
badge: '/favicon.svg',
vibrate: [200, 100, 200],
data: {
timestamp: Date.now(),
url: '/'
},
actions: [
{
action: 'view',
title: 'View',
icon: '/favicon.svg'
},
{
action: 'close',
title: 'Close'
}
]
}
event.waitUntil(
self.registration.showNotification('Miracles in Motion', options)
)
})
// Notification click handling
self.addEventListener('notificationclick', event => {
event.notification.close()
if (event.action === 'view') {
event.waitUntil(
clients.openWindow(event.notification.data.url)
)
}
})