From af7464be43a9f2e38a3cc8b388ecaa9281bd0cd9 Mon Sep 17 00:00:00 2001 From: Hanzo_dev <2002samudiojohan@gmail.com> Date: Tue, 3 Mar 2026 20:51:17 -0500 Subject: [PATCH] fix: resolve blank screen on return from external links --- frontend/src/App.vue | 45 +++++++++++++++++-- frontend/src/stores/shuttle.ts | 13 ++++++ frontend/src/stores/taxi.ts | 13 ++++++ frontend/src/views/DiscoverView.vue | 19 +++++--- frontend/src/views/MapView.vue | 30 ++++++++++--- .../src/views/transporte/TaxisLocales.vue | 3 +- .../src/views/transporte/ViajesTuristicos.vue | 3 +- 7 files changed, 108 insertions(+), 18 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 3b144ef..ba384eb 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -21,6 +21,15 @@ const isAuthScreen = computed(() => { }) let lastHiddenAt: number | null = null +let refocusDebounceTimer: ReturnType | null = null + +function dispatchRefocus(reason: string) { + // Debounce: no disparar dos eventos seguidos en menos de 1 segundo + if (refocusDebounceTimer) return + refocusDebounceTimer = setTimeout(() => { refocusDebounceTimer = null }, 1000) + console.log(`SIBU | App refocus — motivo: ${reason}`) + window.dispatchEvent(new CustomEvent('app-refocus')) +} function handleVisibilityChange() { if (document.visibilityState === 'hidden') { @@ -29,10 +38,32 @@ function handleVisibilityChange() { } if (document.visibilityState === 'visible' && lastHiddenAt !== null) { const secondsAway = (Date.now() - lastHiddenAt) / 1000 - // Si pasaron más de 3 segundos, asumimos que el proceso pudo ser suspendido - if (secondsAway > 3) { - console.log(`SIBU | App recuperada tras ${secondsAway.toFixed(1)}s. Disparando refocus...`) - window.dispatchEvent(new CustomEvent('app-refocus')) + // Umbral bajo (1 s) para capturar retornos rápidos desde Google Maps / links externos + if (secondsAway > 1) { + dispatchRefocus(`visibilitychange tras ${secondsAway.toFixed(1)}s`) + } + lastHiddenAt = null + } +} + +// Fallback: 'pageshow' se dispara al volver desde el bfcache (back-forward cache) +// Muy común en Safari/iOS cuando se abre un link externo y se regresa +function handlePageShow(event: PageTransitionEvent) { + if (event.persisted) { + // La página fue restaurada desde bfcache — siempre necesita refocus + dispatchRefocus('pageshow persisted (bfcache)') + } +} + +// Fallback: 'focus' en window cubre casos donde visibilitychange no se dispara +// (algunos navegadores Android al volver de otra app) +let windowFocusTimer: ReturnType | null = null +function handleWindowFocus() { + // Solo si la pestaña estuvo oculta antes + if (lastHiddenAt !== null) { + const secondsAway = (Date.now() - lastHiddenAt) / 1000 + if (secondsAway > 1) { + dispatchRefocus(`window focus tras ${secondsAway.toFixed(1)}s`) } lastHiddenAt = null } @@ -49,10 +80,16 @@ onMounted(() => { favoritesStore.loadFavorites() } document.addEventListener('visibilitychange', handleVisibilityChange) + window.addEventListener('pageshow', handlePageShow) + window.addEventListener('focus', handleWindowFocus) }) onUnmounted(() => { document.removeEventListener('visibilitychange', handleVisibilityChange) + window.removeEventListener('pageshow', handlePageShow) + window.removeEventListener('focus', handleWindowFocus) + if (refocusDebounceTimer) clearTimeout(refocusDebounceTimer) + if (windowFocusTimer) clearTimeout(windowFocusTimer) }) diff --git a/frontend/src/stores/shuttle.ts b/frontend/src/stores/shuttle.ts index 11cf0ef..68707d5 100644 --- a/frontend/src/stores/shuttle.ts +++ b/frontend/src/stores/shuttle.ts @@ -26,12 +26,25 @@ export const useShuttleStore = defineStore('shuttle', () => { } } + // Recarga silenciosa: no activa el spinner (para refocus desde background) + async function silentReload() { + if (shuttles.value.length === 0) { return loadShuttles() } + error.value = null + try { + shuttles.value = await shuttlesService.getAllShuttles(filters.value) + } catch (e) { + console.error('Error silent-reloading shuttles:', e) + // No mostrar error si ya hay datos + } + } + return { shuttles, isLoading, error, filters, loadShuttles, + silentReload, } }, { persist: { diff --git a/frontend/src/stores/taxi.ts b/frontend/src/stores/taxi.ts index ebbdcdb..07b823f 100644 --- a/frontend/src/stores/taxi.ts +++ b/frontend/src/stores/taxi.ts @@ -31,6 +31,18 @@ export const useTaxiStore = defineStore('taxi', () => { loadTaxis() } + // Recarga silenciosa: no activa el spinner (para refocus desde background) + async function silentReload() { + if (taxis.value.length === 0) { return loadTaxis() } + error.value = null + try { + taxis.value = await taxisService.getAllTaxis(filters.value) + } catch (e) { + console.error('Error silent-reloading taxis:', e) + // No mostrar error si ya hay datos + } + } + return { taxis, isLoading, @@ -38,6 +50,7 @@ export const useTaxiStore = defineStore('taxi', () => { filters, loadTaxis, setFilters, + silentReload, } }, { persist: { diff --git a/frontend/src/views/DiscoverView.vue b/frontend/src/views/DiscoverView.vue index e0747b9..82b268d 100644 --- a/frontend/src/views/DiscoverView.vue +++ b/frontend/src/views/DiscoverView.vue @@ -12,7 +12,7 @@ import AuthGuard from '@/components/common/AuthGuard.vue' const router = useRouter() const { t } = useI18n() const businesses = ref([]) -const isLoading = ref(true) +const isLoading = ref(false) const error = ref(null) const searchQuery = ref('') const selectedCategory = ref('Todas') @@ -41,21 +41,28 @@ function catIcon(cat: string) { return CATEGORY_META[cat]?.icon ?? 'place' } -async function loadBusinesses() { - isLoading.value = true +async function loadBusinesses(silent = false) { + // Modo 'silencioso': si ya tenemos datos, no mostrar spinner — solo refrescar en fondo + if (!silent || businesses.value.length === 0) { + isLoading.value = true + } error.value = null try { businesses.value = await businessService.getAllBusinesses() } catch (e) { console.error('Error loading businesses:', e) - error.value = t('discover.error') + // Solo mostrar error si no hay datos previos + if (businesses.value.length === 0) { + error.value = t('discover.error') + } } finally { isLoading.value = false } } function handleRefocus() { - loadBusinesses() + // Recarga silenciosa: no congela la UI si ya hay datos visibles + loadBusinesses(true) } onMounted(() => { @@ -179,7 +186,7 @@ function resetFilters() {
error_outline

{{ error }}

- diff --git a/frontend/src/views/MapView.vue b/frontend/src/views/MapView.vue index d6b8367..1aa4b70 100644 --- a/frontend/src/views/MapView.vue +++ b/frontend/src/views/MapView.vue @@ -143,10 +143,28 @@ async function fetchData() { updateActiveUnits(); } -function handleRefocus() { +async function handleRefocus() { + // Refrescar datos en fondo fetchData(); + + await nextTick(); + if (map.value) { - google.maps.event.trigger(map.value, 'resize'); + // El mapa sigue vivo — solo redimensionar y actualizar + try { + google.maps.event.trigger(map.value, 'resize'); + } catch (_) { /* ignorar si google no disponible */ } + updateActiveUnits(); + } else { + // El mapa fue destruido por el browser al suspender la pestaña — reinicializar + console.log('SIBU | Mapa perdido tras refocus, reinicializando...'); + if (isLoaded.value) { + await initializeMap(); + } else { + const unwatch = watch(isLoaded, async (loaded) => { + if (loaded) { await initializeMap(); unwatch(); } + }); + } } } @@ -154,7 +172,7 @@ function handleRefocus() { onMounted(async () => { analyticsService.logEvent({ event_name: 'screen_view', properties: { screen_name: 'Map' } }) window.addEventListener('app-refocus', handleRefocus); - + await fetchData(); const queryRouteId = router.currentRoute.value.query.routeId as string; @@ -179,7 +197,6 @@ onMounted(async () => { unitFetchInterval.value = setInterval(updateActiveUnits, 15000); startCarousel(); - // 🛰️ RESIZE FIX: Trigger map resize when app becomes visible again document.addEventListener('visibilitychange', handleVisibilityChange); }); @@ -192,8 +209,9 @@ onUnmounted(() => { function handleVisibilityChange() { if (document.visibilityState === 'visible' && map.value) { - console.log('SIBU | App visible, redimensionando mapa...'); - google.maps.event.trigger(map.value, 'resize'); + try { + google.maps.event.trigger(map.value, 'resize'); + } catch (_) { /* ignorar */ } updateActiveUnits(); } } diff --git a/frontend/src/views/transporte/TaxisLocales.vue b/frontend/src/views/transporte/TaxisLocales.vue index 7b10890..ac6e9e8 100644 --- a/frontend/src/views/transporte/TaxisLocales.vue +++ b/frontend/src/views/transporte/TaxisLocales.vue @@ -23,7 +23,8 @@ function fetchData() { } function handleRefocus() { - fetchData() + // Recarga silenciosa: no congela la UI si ya hay datos + taxiStore.silentReload() } onMounted(async () => { diff --git a/frontend/src/views/transporte/ViajesTuristicos.vue b/frontend/src/views/transporte/ViajesTuristicos.vue index ceec74d..81b1dba 100644 --- a/frontend/src/views/transporte/ViajesTuristicos.vue +++ b/frontend/src/views/transporte/ViajesTuristicos.vue @@ -34,7 +34,8 @@ function fetchData() { } function handleRefocus() { - fetchData() + // Recarga silenciosa: no congela la UI si ya hay datos + shuttleStore.silentReload() } onMounted(async () => {