feat(RECIPE-0009): complete iteration 1 — footer status bar, icon-only buttons
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
import NotificationSettings from './components/NotificationSettings.svelte';
|
import NotificationSettings from './components/NotificationSettings.svelte';
|
||||||
import { replaceState } from '$app/navigation';
|
import { replaceState } from '$app/navigation';
|
||||||
import { pushNotificationManager } from '$lib/client/PushNotificationManager';
|
import { pushNotificationManager } from '$lib/client/PushNotificationManager';
|
||||||
|
import type { NotificationState } from '$lib/client/PushNotificationManager';
|
||||||
|
|
||||||
let items = $state<QueueItem[]>([]);
|
let items = $state<QueueItem[]>([]);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
let connectionStatus = $state<'connecting' | 'connected' | 'disconnected'>('disconnected');
|
let connectionStatus = $state<'connecting' | 'connected' | 'disconnected'>('disconnected');
|
||||||
let lastPing = $state<string | null>(null);
|
let lastPing = $state<string | null>(null);
|
||||||
let hasAttemptedAutoSubscribe = $state(false);
|
let hasAttemptedAutoSubscribe = $state(false);
|
||||||
|
let notificationViewModel = $state<NotificationState | null>(null);
|
||||||
|
|
||||||
// Get highlighted item ID from URL params (when redirected from Share page)
|
// Get highlighted item ID from URL params (when redirected from Share page)
|
||||||
let highlightId = $derived($page.url.searchParams.get('highlight'));
|
let highlightId = $derived($page.url.searchParams.get('highlight'));
|
||||||
@@ -37,11 +39,16 @@
|
|||||||
return items.filter(item => item.status === filter);
|
return items.filter(item => item.status === filter);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let unsubscribeNotifications: (() => void) | undefined;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await loadQueueItems();
|
await loadQueueItems();
|
||||||
if (browser) {
|
if (browser) {
|
||||||
startSSEConnection();
|
startSSEConnection();
|
||||||
setupAutoSubscribe();
|
setupAutoSubscribe();
|
||||||
|
unsubscribeNotifications = pushNotificationManager.onStateChange((newState) => {
|
||||||
|
notificationViewModel = newState;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -51,6 +58,8 @@
|
|||||||
eventSource.close();
|
eventSource.close();
|
||||||
connectionStatus = 'disconnected';
|
connectionStatus = 'disconnected';
|
||||||
}
|
}
|
||||||
|
// Add notification state cleanup
|
||||||
|
unsubscribeNotifications?.();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadQueueItems() {
|
async function loadQueueItems() {
|
||||||
@@ -282,25 +291,29 @@
|
|||||||
<button
|
<button
|
||||||
onclick={loadQueueItems}
|
onclick={loadQueueItems}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
class="flex items-center space-x-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 disabled:opacity-50 transition-colors"
|
title="Refresh queue"
|
||||||
|
aria-label="Refresh queue"
|
||||||
|
class="flex items-center p-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 disabled:opacity-50 transition-colors"
|
||||||
>
|
>
|
||||||
<svg class="w-4 h-4 {loading ? 'animate-spin' : ''}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5 {loading ? '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>
|
<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>
|
</svg>
|
||||||
<span>Refresh</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add Recipe Button (always visible) -->
|
<!-- Add Recipe Button (icon-only, visible when items exist) -->
|
||||||
<a
|
{#if items.length > 0}
|
||||||
href="/share"
|
<a
|
||||||
class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium"
|
href="/share"
|
||||||
>
|
title="Add recipe URL"
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
aria-label="Add recipe URL"
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
class="inline-flex items-center p-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
</svg>
|
>
|
||||||
Add Recipe URL
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</a>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
@@ -364,28 +377,53 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Notification Settings - Always visible -->
|
<!-- Notification Settings - Always visible -->
|
||||||
<div class="mt-8">
|
<div class="mt-8" data-notification-settings>
|
||||||
<NotificationSettings />
|
<NotificationSettings />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Connection Status -->
|
<!-- Footer Status Bar (icons only) -->
|
||||||
<div class="fixed bottom-4 right-4">
|
<div class="fixed bottom-0 left-0 right-0 bg-white border-t shadow-lg z-50">
|
||||||
<div class="flex items-center space-x-2 px-3 py-2 bg-white border rounded-lg shadow-sm text-sm">
|
<div class="mx-auto max-w-6xl px-6 py-3 flex items-center justify-between">
|
||||||
<div class="w-2 h-2 rounded-full {
|
<!-- Notification Status Icon (left) -->
|
||||||
connectionStatus === 'connected' ? 'bg-green-400' :
|
<button
|
||||||
connectionStatus === 'connecting' ? 'bg-yellow-400' :
|
onclick={() => {
|
||||||
'bg-red-400'
|
// Scroll to NotificationSettings component
|
||||||
}"></div>
|
document.querySelector('[data-notification-settings]')?.scrollIntoView({ behavior: 'smooth' });
|
||||||
<span class="text-gray-600">
|
}}
|
||||||
{connectionStatus === 'connected' ? 'Live updates' :
|
title={notificationViewModel?.subscribed ? 'Notifications enabled' : notificationViewModel?.supported ? 'Notifications disabled' : 'Notifications not supported'}
|
||||||
connectionStatus === 'connecting' ? 'Connecting...' :
|
aria-label="Notification status"
|
||||||
'Disconnected'}
|
class="p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
||||||
</span>
|
>
|
||||||
{#if lastPing}
|
{#if !notificationViewModel?.supported || notificationViewModel?.permission === 'denied'}
|
||||||
<span class="text-xs text-gray-400">
|
<!-- Not supported / denied - bell with slash -->
|
||||||
({new Date(lastPing).toLocaleTimeString()})
|
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</span>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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"></path>
|
||||||
{/if}
|
</svg>
|
||||||
|
{:else if notificationViewModel?.subscribed}
|
||||||
|
<!-- Enabled - bell icon (green) -->
|
||||||
|
<svg class="w-5 h-5 text-green-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>
|
||||||
|
{:else}
|
||||||
|
<!-- Disabled - bell icon (gray) -->
|
||||||
|
<svg class="w-5 h-5 text-gray-400" 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>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Live Update Indicator (right) -->
|
||||||
|
<div
|
||||||
|
title={connectionStatus === 'connected' ? 'Live updates active' : connectionStatus === 'connecting' ? 'Connecting to live updates...' : 'Live updates disconnected'}
|
||||||
|
aria-label="Live update status"
|
||||||
|
class="flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<div class="w-2 h-2 rounded-full {
|
||||||
|
connectionStatus === 'connected' ? 'bg-green-400' :
|
||||||
|
connectionStatus === 'connecting' ? 'bg-yellow-400' :
|
||||||
|
'bg-red-400'
|
||||||
|
}"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user