feat(RECIPE-0009): complete iteration 1 — footer status bar, icon-only buttons

This commit is contained in:
Giancarmine Salucci
2026-02-18 10:35:51 +01:00
parent c98a2407a7
commit 08602073ac

View File

@@ -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>