Add sales automation stack: strategy docs, MCP scripts, PWA refinements
- 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
This commit is contained in:
+197
@@ -0,0 +1,197 @@
|
||||
// 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');
|
||||
Reference in New Issue
Block a user