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