- Fix InvalidCharacterError in push notifications with proper VAPID key validation - Add attractive PWA install prompt component with cross-browser support - Make notification settings always visible regardless of queue status - Implement PWA install manager with user engagement detection - Use SvelteKit navigation APIs instead of browser history API - Add comprehensive error handling and logging - Include cross-browser compatibility and responsive design - Add development tooling improvements Fixes push notification bugs and significantly improves PWA user experience with modern, accessible interface components and proper error handling.
182 lines
7.4 KiB
Svelte
182 lines
7.4 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import { pushNotificationManager, type NotificationState } from '$lib/client/PushNotificationManager';
|
|
|
|
let state = $state<NotificationState>({
|
|
supported: false,
|
|
permission: 'default',
|
|
subscribed: false,
|
|
loading: false,
|
|
error: null
|
|
});
|
|
|
|
let unsubscribe: (() => void) | null = null;
|
|
|
|
onMount(() => {
|
|
// Subscribe to state changes
|
|
unsubscribe = pushNotificationManager.onStateChange((newState) => {
|
|
state = newState;
|
|
});
|
|
|
|
return () => {
|
|
unsubscribe?.();
|
|
};
|
|
});
|
|
|
|
async function handleToggle() {
|
|
await pushNotificationManager.toggleSubscription();
|
|
}
|
|
|
|
function getStatusText(): string {
|
|
if (!state.supported) return 'Not supported';
|
|
if (state.permission === 'denied') return 'Permission denied';
|
|
if (state.subscribed) return 'Enabled';
|
|
if (state.permission === 'granted') return 'Available';
|
|
return 'Permission needed';
|
|
}
|
|
|
|
function getStatusColor(): string {
|
|
if (!state.supported || state.permission === 'denied') return 'text-red-600';
|
|
if (state.subscribed) return 'text-green-600';
|
|
return 'text-yellow-600';
|
|
}
|
|
|
|
function getButtonText(): string {
|
|
if (state.loading) return 'Working...';
|
|
if (state.subscribed) return 'Disable Notifications';
|
|
return 'Enable Notifications';
|
|
}
|
|
|
|
function canToggle(): boolean {
|
|
return state.supported && state.permission !== 'denied' && !state.loading;
|
|
}
|
|
</script>
|
|
|
|
<div class="bg-white border rounded-lg shadow-sm p-6">
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<div class="flex items-center space-x-2 mb-2">
|
|
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-5 5-5-5h5v-8a1 1 0 011-1h8a1 1 0 011 1v1a1 1 0 01-1 1h-7v7z"></path>
|
|
</svg>
|
|
<h3 class="text-lg font-medium text-gray-900">Push Notifications</h3>
|
|
</div>
|
|
|
|
<p class="text-sm text-gray-600 mb-4">
|
|
Get notified when your recipe extractions complete, even when InstaRecipe is not open.
|
|
{#if typeof window !== 'undefined'}
|
|
<!-- Check if we're on the homepage and queue appears empty -->
|
|
{#if window.location.pathname === '/' && !document.querySelector('[data-queue-item]')}
|
|
Start by adding some Instagram recipe URLs to see notifications in action!
|
|
{/if}
|
|
{/if}
|
|
</p>
|
|
|
|
<!-- Status -->
|
|
<div class="flex items-center space-x-2 mb-4">
|
|
<span class="text-sm text-gray-500">Status:</span>
|
|
<span class="text-sm font-medium {getStatusColor()}">
|
|
{getStatusText()}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Error Message -->
|
|
{#if state.error}
|
|
<div class="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg">
|
|
<div class="flex items-start space-x-2">
|
|
<svg class="w-4 h-4 text-red-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 15.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
|
</svg>
|
|
<div>
|
|
<div class="text-sm font-medium text-red-800">Error</div>
|
|
<div class="text-sm text-red-700">{state.error}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Browser Support Info -->
|
|
{#if !state.supported}
|
|
<div class="mb-4 p-3 bg-gray-50 border border-gray-200 rounded-lg">
|
|
<div class="flex items-start space-x-2">
|
|
<svg class="w-4 h-4 text-gray-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
<div>
|
|
<div class="text-sm font-medium text-gray-800">Not Supported</div>
|
|
<div class="text-sm text-gray-600">
|
|
Your browser doesn't support push notifications or the site is not running over HTTPS.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Permission Denied Info -->
|
|
{#if state.permission === 'denied'}
|
|
<div class="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
|
<div class="flex items-start space-x-2">
|
|
<svg class="w-4 h-4 text-yellow-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 15.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
|
</svg>
|
|
<div>
|
|
<div class="text-sm font-medium text-yellow-800">Permission Denied</div>
|
|
<div class="text-sm text-yellow-700">
|
|
You've blocked notifications for this site. Please enable them in your browser settings to receive updates.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Features List -->
|
|
{#if state.supported && state.permission !== 'denied'}
|
|
<div class="mb-4">
|
|
<div class="text-sm text-gray-600 mb-2">You'll receive notifications for:</div>
|
|
<ul class="text-sm text-gray-600 space-y-1">
|
|
<li class="flex items-center space-x-2">
|
|
<svg class="w-3 h-3 text-green-500" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
<span>✅ Successful recipe extractions</span>
|
|
</li>
|
|
<li class="flex items-center space-x-2">
|
|
<svg class="w-3 h-3 text-green-500" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
<span>❌ Failed extractions (with retry option)</span>
|
|
</li>
|
|
<li class="flex items-center space-x-2">
|
|
<svg class="w-3 h-3 text-green-500" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
<span>🔗 Direct links to view in Tandoor</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Toggle Button -->
|
|
<div class="ml-6">
|
|
<button
|
|
onclick={handleToggle}
|
|
disabled={!canToggle()}
|
|
class="flex items-center space-x-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors {state.subscribed
|
|
? 'bg-red-100 text-red-700 hover:bg-red-200 disabled:opacity-50'
|
|
: 'bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50'} disabled:cursor-not-allowed"
|
|
>
|
|
{#if state.loading}
|
|
<svg class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
|
</svg>
|
|
{:else}
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={state.subscribed ? "M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18.364 5.636M5.636 18.364l12.728-12.728" : "M15 17h5l-5 5-5-5h5v-8a1 1 0 011-1h8a1 1 0 011 1v1a1 1 0 01-1 1h-7v7z"}></path>
|
|
</svg>
|
|
{/if}
|
|
<span>{getButtonText()}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div> |