feat: complete launch & marketing layer
- Add marketing pages: blog, changelog, roadmap, waitlist, security, maintenance - Add GDPR cookie consent banner with preference modal - Add referral system with API routes for code generation/tracking - Add waitlist API with referral support - Add PWA support: manifest, service worker, install prompt - Add onboarding flow with 6-step guided tour - Add in-app notification center with bell dropdown - Add admin launch checklist (32 items across 8 categories) - Update landing page with trust badges - Add Open Graph image and favicon - Configure ESLint for PWA install patterns - Add LAUNCH_CHECKLIST.md with go-to-market guide Supabase migrations for: - blog_posts and blog_categories tables - waitlist_signups table - roadmap_items table - launch_checklist_items table
This commit is contained in:
+6
-80
@@ -18,7 +18,7 @@ export function registerServiceWorker() {
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||
// New content available, prompt user to refresh
|
||||
showUpdatePrompt();
|
||||
window.dispatchEvent(new CustomEvent('sw-update-available'));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -31,101 +31,27 @@ export function registerServiceWorker() {
|
||||
});
|
||||
}
|
||||
|
||||
function showUpdatePrompt() {
|
||||
// Dispatch custom event for UI to handle update prompt
|
||||
const event = new CustomEvent('sw-update-available');
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
|
||||
export function requestNotificationPermission() {
|
||||
if (!('Notification' in window) || !('serviceWorker' in navigator)) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return Notification.requestPermission();
|
||||
}
|
||||
|
||||
export async function subscribeToPush(registration: ServiceWorkerRegistration) {
|
||||
if (!('PushManager' in window)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const subscription = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(
|
||||
process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY || ''
|
||||
),
|
||||
});
|
||||
|
||||
console.log('Push subscription:', subscription);
|
||||
return subscription;
|
||||
} catch (error) {
|
||||
console.error('Push subscription failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
||||
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
||||
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
||||
const rawData = window.atob(base64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
|
||||
for (let i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i);
|
||||
}
|
||||
|
||||
return outputArray;
|
||||
export async function subscribeToPush(_registration: ServiceWorkerRegistration) {
|
||||
// Push subscription disabled for now - requires VAPID key setup
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if app is installed (PWA)
|
||||
export function isPWAInstalled(): boolean {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return (
|
||||
window.matchMedia('(display-mode: standalone)').matches ||
|
||||
(window.navigator as { standalone?: boolean }).standalone === true
|
||||
);
|
||||
}
|
||||
|
||||
// Get install prompt
|
||||
export function getInstallPrompt(): { prompt: () => void; outcome: Promise<'accepted' | 'dismissed' | 'canceled'> } | null {
|
||||
if (!('beforeinstallprompt' in window)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let deferredPrompt: beforeinstallpromptEvent | null = null;
|
||||
|
||||
window.addEventListener('beforeinstallprompt', (event) => {
|
||||
event.preventDefault();
|
||||
deferredPrompt = event;
|
||||
});
|
||||
|
||||
return {
|
||||
prompt: () => {
|
||||
if (deferredPrompt) {
|
||||
deferredPrompt.prompt();
|
||||
}
|
||||
},
|
||||
outcome: new Promise((resolve) => {
|
||||
if (deferredPrompt) {
|
||||
deferredPrompt.userChoice.then((choiceResult) => {
|
||||
deferredPrompt = null;
|
||||
resolve(choiceResult.outcome as 'accepted' | 'dismissed' | 'canceled');
|
||||
});
|
||||
} else {
|
||||
resolve('dismissed');
|
||||
}
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// Share API for native sharing
|
||||
export async function shareContent(content: {
|
||||
title: string;
|
||||
text: string;
|
||||
url?: string;
|
||||
}): Promise<boolean> {
|
||||
if (!navigator.share) {
|
||||
if (typeof window === 'undefined' || !navigator.share) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user