a440160249
- SALES_STRATEGY.md: $1K/day plan (37 orders @ $27.50 AOV, 1,700 visits) - TRIPWIRE_FUNNEL.md: 7-email Mautic sequence + lead scoring - MONETIZATION.md: 9 monetization options for Nūr PWA (no ads, no registration) - EXECUTABLE_PLAN.md: MCP-executable automation plan - DAILY_OPS.md: daily KPI checklist (visits, orders, sends, recoveries) - scripts/: automation.py, mautic_setup.py, gumroad_setup.py, scout_scrape.py, analytics.py, crontab - quality_leads.json/.csv: 12 enriched Islamic-tech leads for Mautic import - content_queue.json: 10 SEO blog posts for Ghost - PWA: icons, sw.js, e2e + vitest coverage, generator scripts
197 lines
5.3 KiB
JavaScript
197 lines
5.3 KiB
JavaScript
// Custom Service Worker for Nūr — Muslim Companion
|
|
// Handles push notifications for prayer times
|
|
|
|
// Workbox precache manifest injection point
|
|
self.__WB_MANIFEST;
|
|
|
|
const CACHE_NAME = 'nur-v1';
|
|
const PRAYER_CACHE = 'prayer-times-cache';
|
|
|
|
// Install event
|
|
self.addEventListener('install', (event) => {
|
|
event.waitUntil(
|
|
caches.open(CACHE_NAME).then((cache) => {
|
|
return cache.addAll([
|
|
'/',
|
|
'/index.html',
|
|
'/manifest.webmanifest',
|
|
'/icon-192.png',
|
|
'/icon-512.png',
|
|
'/favicon.svg',
|
|
'/apple-touch-icon.png'
|
|
]);
|
|
})
|
|
);
|
|
self.skipWaiting();
|
|
});
|
|
|
|
// Activate event
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil(
|
|
caches.keys().then((cacheNames) => {
|
|
return Promise.all(
|
|
cacheNames
|
|
.filter((name) => name !== CACHE_NAME && name !== PRAYER_CACHE)
|
|
.map((name) => caches.delete(name))
|
|
);
|
|
})
|
|
);
|
|
self.clients.claim();
|
|
});
|
|
|
|
// Fetch event - network first for API, cache first for static
|
|
self.addEventListener('fetch', (event) => {
|
|
const url = new URL(event.request.url);
|
|
|
|
// API calls - network first with cache fallback
|
|
if (url.origin === 'https://api.aladhan.com' || url.origin === 'https://api.alquran.cloud') {
|
|
event.respondWith(
|
|
fetch(event.request)
|
|
.then((response) => {
|
|
const responseClone = response.clone();
|
|
caches.open(PRAYER_CACHE).then((cache) => {
|
|
cache.put(event.request, responseClone);
|
|
});
|
|
return response;
|
|
})
|
|
.catch(() => caches.match(event.request))
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Static assets - cache first
|
|
event.respondWith(
|
|
caches.match(event.request).then((cachedResponse) => {
|
|
if (cachedResponse) return cachedResponse;
|
|
return fetch(event.request).then((response) => {
|
|
const responseClone = response.clone();
|
|
caches.open(CACHE_NAME).then((cache) => {
|
|
cache.put(event.request, responseClone);
|
|
});
|
|
return response;
|
|
});
|
|
})
|
|
);
|
|
});
|
|
|
|
// Push event - handle push notifications
|
|
self.addEventListener('push', (event) => {
|
|
if (!event.data) return;
|
|
|
|
const data = event.data.json();
|
|
const options = {
|
|
body: data.body,
|
|
icon: '/icon-192.png',
|
|
badge: '/icon-192.png',
|
|
vibrate: [200, 100, 200],
|
|
tag: data.tag || 'prayer-notification',
|
|
renotify: true,
|
|
requireInteraction: true,
|
|
actions: [
|
|
{ action: 'open', title: 'Open App' },
|
|
{ action: 'dismiss', title: 'Dismiss' }
|
|
],
|
|
data: data.data || {}
|
|
};
|
|
|
|
event.waitUntil(
|
|
self.registration.showNotification(data.title, options)
|
|
);
|
|
});
|
|
|
|
// Notification click event
|
|
self.addEventListener('notificationclick', (event) => {
|
|
event.notification.close();
|
|
|
|
if (event.action === 'dismiss') return;
|
|
|
|
event.waitUntil(
|
|
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => {
|
|
// If app is already open, focus it
|
|
for (const client of clientList) {
|
|
if (client.url.includes(self.location.origin) && 'focus' in client) {
|
|
return client.focus();
|
|
}
|
|
}
|
|
// Otherwise open new window
|
|
if (clients.openWindow) {
|
|
return clients.openWindow('/');
|
|
}
|
|
})
|
|
);
|
|
});
|
|
|
|
// Background sync for periodic prayer time updates
|
|
self.addEventListener('sync', (event) => {
|
|
if (event.tag === 'prayer-times-sync') {
|
|
event.waitUntil(syncPrayerTimes());
|
|
}
|
|
});
|
|
|
|
// Periodic background sync (if supported)
|
|
self.addEventListener('periodicsync', (event) => {
|
|
if (event.tag === 'prayer-times-periodic') {
|
|
event.waitUntil(syncPrayerTimes());
|
|
}
|
|
});
|
|
|
|
async function syncPrayerTimes() {
|
|
try {
|
|
// Get cached prayer times or fetch new ones
|
|
const cache = await caches.open(PRAYER_CACHE);
|
|
const cached = await cache.match('/prayer-times');
|
|
|
|
if (cached) {
|
|
const data = await cached.json();
|
|
scheduleNotifications(data);
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to sync prayer times:', e);
|
|
}
|
|
}
|
|
|
|
function scheduleNotifications(prayerData) {
|
|
const now = Date.now();
|
|
const prayers = ['Fajr', 'Dhuhr', 'Asr', 'Maghrib', 'Isha'];
|
|
|
|
prayers.forEach((prayer) => {
|
|
const prayerTimeStr = prayerData.timings[prayer];
|
|
if (!prayerTimeStr) return;
|
|
|
|
const [hours, minutes] = prayerTimeStr.split(':').map(Number);
|
|
const prayerDate = new Date();
|
|
prayerDate.setHours(hours, minutes, 0, 0);
|
|
|
|
const timeUntilPrayer = prayerDate.getTime() - now;
|
|
|
|
// Schedule notification 5 minutes before prayer
|
|
const notifyTime = timeUntilPrayer - 5 * 60 * 1000;
|
|
|
|
if (notifyTime > 0 && notifyTime < 24 * 60 * 60 * 1000) {
|
|
setTimeout(() => {
|
|
self.registration.showNotification(`Time for ${prayer}`, {
|
|
body: `It's time for ${prayer} prayer (${prayerTimeStr})`,
|
|
icon: '/icon-192.png',
|
|
badge: '/icon-192.png',
|
|
vibrate: [200, 100, 200],
|
|
tag: `prayer-${prayer}`,
|
|
renotify: true,
|
|
requireInteraction: true
|
|
});
|
|
}, notifyTime);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Message event - communicate with main thread
|
|
self.addEventListener('message', (event) => {
|
|
if (event.data.type === 'SCHEDULE_PRAYERS') {
|
|
scheduleNotifications(event.data.payload);
|
|
}
|
|
|
|
if (event.data.type === 'SKIP_WAITING') {
|
|
self.skipWaiting();
|
|
}
|
|
});
|
|
|
|
console.log('[Nūr SW] Service Worker loaded'); |