/** * Offline Indicator Component * * Displays network status and sync state in the header. * Shows: Online, Offline, Syncing states with appropriate colors. */ const { ref, computed, onMounted, onUnmounted, watch } = Vue; export default { name: 'OfflineIndicator', props: { // Whether offline mode is enabled in settings offlineModeEnabled: { type: Boolean, default: false }, // Number of pending changes pendingCount: { type: Number, default: 0 }, // Whether sync is currently running isSyncing: { type: Boolean, default: false }, // Data freshness level: 'fresh', 'stale', 'old', 'unknown' freshness: { type: String, default: 'unknown' } }, emits: ['sync-click'], setup(props, { emit }) { const isOnline = ref(navigator.onLine); // Update online status const updateOnlineStatus = () => { isOnline.value = navigator.onLine; }; onMounted(() => { window.addEventListener('online', updateOnlineStatus); window.addEventListener('offline', updateOnlineStatus); }); onUnmounted(() => { window.removeEventListener('online', updateOnlineStatus); window.removeEventListener('offline', updateOnlineStatus); }); // Computed display state const displayState = computed(() => { if (!props.offlineModeEnabled) { return isOnline.value ? 'online' : 'offline-no-cache'; } if (props.isSyncing) { return 'syncing'; } if (!isOnline.value) { return 'offline'; } if (props.pendingCount > 0) { return 'pending'; } return 'online'; }); // Status text const statusText = computed(() => { switch (displayState.value) { case 'syncing': return 'Synchronisiere...'; case 'offline': return 'Offline'; case 'offline-no-cache': return 'Keine Verbindung'; case 'pending': return `${props.pendingCount} ausstehend`; case 'online': return props.offlineModeEnabled ? 'Synchronisiert' : ''; default: return ''; } }); // Status icon const statusIcon = computed(() => { switch (displayState.value) { case 'syncing': return 'sync'; case 'offline': case 'offline-no-cache': return 'cloud-off'; case 'pending': return 'cloud-upload'; case 'online': return 'cloud-check'; default: return 'cloud'; } }); // Status color classes const statusClasses = computed(() => { switch (displayState.value) { case 'syncing': return 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'; case 'offline': return 'bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300'; case 'offline-no-cache': return 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300'; case 'pending': return 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300'; case 'online': return 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300'; default: return 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'; } }); // Freshness indicator color const freshnessColor = computed(() => { switch (props.freshness) { case 'fresh': return 'bg-green-500'; case 'stale': return 'bg-yellow-500'; case 'old': return 'bg-red-500'; default: return 'bg-gray-400'; } }); // Click handler const handleClick = () => { if (isOnline.value && props.pendingCount > 0) { emit('sync-click'); } }; // Should show (only show if offline mode enabled or offline) const shouldShow = computed(() => { return props.offlineModeEnabled || !isOnline.value; }); return { isOnline, displayState, statusText, statusIcon, statusClasses, freshnessColor, handleClick, shouldShow }; }, template: `
{{ statusText }}
` };