Implement Smart Location: auto-detect user location if preference is enabled, hide location button, and handle permission denial by resetting preference

This commit is contained in:
2026-03-01 12:15:08 -05:00
parent d0d75b8c98
commit 4d7b472c6c
20 changed files with 852 additions and 344 deletions

View File

@ -2,20 +2,22 @@
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useFavoritesStore } from '@/stores/favorites'
import { useI18n } from 'vue-i18n'
import { getImageUrl as utilGetImageUrl } from '@/utils/imageUrl'
const router = useRouter()
const { t } = useI18n()
const favoritesStore = useFavoritesStore()
const selectedFilter = ref('all')
const filters = [
{ key: 'all', label: 'Todos', icon: 'star' },
{ key: 'routes', label: 'Buses', icon: 'directions_bus' },
{ key: 'taxis', label: 'Taxis', icon: 'local_taxi' },
{ key: 'businesses',label: 'Comercios', icon: 'store' },
{ key: 'coupons', label: 'Ofertas', icon: 'confirmation_number' },
{ key: 'stops', label: 'Paradas', icon: 'location_on' },
]
const filters = computed(() => [
{ key: 'all', label: t('common.all'), icon: 'star' },
{ key: 'routes', label: t('favorites.tabs.routes'), icon: 'directions_bus' },
{ key: 'taxis', label: t('favorites.tabs.taxis'), icon: 'local_taxi' },
{ key: 'businesses',label: t('favorites.tabs.businesses'), icon: 'store' },
{ key: 'coupons', label: t('favorites.tabs.coupons'), icon: 'confirmation_number' },
{ key: 'stops', label: t('navigation.routes'), icon: 'location_on' }, // Reusing navigation.routes or adding a specific one
])
onMounted(async () => {
await favoritesStore.loadFavorites()
@ -67,9 +69,9 @@ const hasVisibleItems = computed(() =>
<!-- Header -->
<header class="fav-header">
<h1 class="fav-title">Mis Favoritos</h1>
<h1 class="fav-title">{{ t('favorites.title') }}</h1>
<p class="fav-count" v-if="totalFavorites > 0">
{{ totalFavorites }} guardado{{ totalFavorites !== 1 ? 's' : '' }}
{{ t('favorites.count', { count: totalFavorites }) }}
</p>
</header>
@ -92,7 +94,7 @@ const hasVisibleItems = computed(() =>
<!-- Loading -->
<div v-if="favoritesStore.isLoading" class="state-center">
<div class="spinner"></div>
<p>Cargando favoritos...</p>
<p>{{ t('common.loading') }}</p>
</div>
<template v-else>
@ -106,18 +108,18 @@ const hasVisibleItems = computed(() =>
<line x1="43" y1="45" x2="57" y2="45" stroke="var(--active-color)" stroke-width="4" stroke-linecap="round"/>
</svg>
</div>
<h2 class="empty-title">Nada guardado aún</h2>
<p class="empty-sub">Explora rutas, taxis y negocios para guardar tus favoritos aquí</p>
<h2 class="empty-title">{{ t('favorites.empty.title') }}</h2>
<p class="empty-sub">{{ t('favorites.empty.description') }}</p>
<button class="cta-btn" @click="router.push('/map')">
<span class="material-icons">explore</span>
Explorar ahora
{{ t('favorites.cta.exploreNow') }}
</button>
</div>
<!-- Empty de categoría -->
<div v-else-if="!hasVisibleItems" class="empty-state empty-state--sm">
<span class="material-icons empty-cat-icon">search_off</span>
<p class="empty-sub">No tienes favoritos en esta categoría</p>
<p class="empty-sub">{{ t('favorites.empty.noResultsCategory') }}</p>
</div>
<!-- Contenido -->
@ -127,7 +129,7 @@ const hasVisibleItems = computed(() =>
<section v-if="visibleRoutes.length > 0" class="fav-section">
<div class="section-label">
<span class="material-icons">directions_bus</span>
<span>Rutas</span>
<span>{{ t('favorites.tabs.routes') }}</span>
</div>
<div class="card-list">
<div
@ -141,9 +143,9 @@ const hasVisibleItems = computed(() =>
</div>
<div class="card-info">
<p class="card-name">{{ item.item_name }}</p>
<p class="card-sub">Toca para ver horarios</p>
<p class="card-sub">{{ t('favorites.viewSchedules') }}</p>
</div>
<button class="heart-btn heart-btn--active" @click.stop="removeFavorite($event, item.item_type, item.item_id)" title="Quitar de favoritos">
<button class="heart-btn heart-btn--active" @click.stop="removeFavorite($event, item.item_type, item.item_id)" :title="t('favorites.removeTitle')">
<span class="material-icons">favorite</span>
</button>
</div>
@ -154,7 +156,7 @@ const hasVisibleItems = computed(() =>
<section v-if="visibleTaxis.length > 0" class="fav-section">
<div class="section-label">
<span class="material-icons">local_taxi</span>
<span>Taxis</span>
<span>{{ t('favorites.tabs.taxis') }}</span>
</div>
<div class="card-list">
<div
@ -168,9 +170,9 @@ const hasVisibleItems = computed(() =>
</div>
<div class="card-info">
<p class="card-name">{{ item.item_name }}</p>
<p class="card-sub">Taxi Ver disponibilidad</p>
<p class="card-sub">{{ t('favorites.availability') }}</p>
</div>
<button class="heart-btn heart-btn--active" @click.stop="removeFavorite($event, item.item_type, item.item_id)" title="Quitar de favoritos">
<button class="heart-btn heart-btn--active" @click.stop="removeFavorite($event, item.item_type, item.item_id)" :title="t('favorites.removeTitle')">
<span class="material-icons">favorite</span>
</button>
</div>
@ -181,7 +183,7 @@ const hasVisibleItems = computed(() =>
<section v-if="visibleBusinesses.length > 0" class="fav-section">
<div class="section-label">
<span class="material-icons">store</span>
<span>Negocios</span>
<span>{{ t('favorites.tabs.businesses') }}</span>
</div>
<div class="biz-grid">
<div
@ -195,11 +197,11 @@ const hasVisibleItems = computed(() =>
<button class="heart-btn heart-btn--active heart-btn--overlay" @click.stop="removeFavorite($event, item.item_type, item.item_id)">
<span class="material-icons">favorite</span>
</button>
<span class="biz-badge">Negocio</span>
<span class="biz-badge">{{ t('favorites.tabs.businesses') }}</span>
</div>
<div class="biz-body">
<p class="card-name">{{ item.item_name }}</p>
<p class="card-sub">Ver detalles </p>
<p class="card-sub">{{ t('favorites.details') }}</p>
</div>
</div>
</div>
@ -209,7 +211,7 @@ const hasVisibleItems = computed(() =>
<section v-if="visibleCoupons.length > 0" class="fav-section">
<div class="section-label">
<span class="material-icons">confirmation_number</span>
<span>Ofertas y Viajes</span>
<span>{{ t('favorites.tabs.coupons') }}</span>
</div>
<div class="card-list">
<div
@ -224,9 +226,9 @@ const hasVisibleItems = computed(() =>
</div>
<div class="card-info">
<p class="card-name">{{ item.item_name }}</p>
<span class="badge-avail">Cupón</span>
<span class="badge-avail">{{ t('favorites.tabs.coupons') }}</span>
</div>
<button class="heart-btn heart-btn--active" @click.stop="removeFavorite($event, item.item_type, item.item_id)" title="Quitar de favoritos">
<button class="heart-btn heart-btn--active" @click.stop="removeFavorite($event, item.item_type, item.item_id)" :title="t('favorites.removeTitle')">
<span class="material-icons">favorite</span>
</button>
</div>
@ -237,7 +239,7 @@ const hasVisibleItems = computed(() =>
<section v-if="visibleStops.length > 0" class="fav-section">
<div class="section-label">
<span class="material-icons">location_on</span>
<span>Paradas</span>
<span>{{ t('navigation.routes') }}</span>
</div>
<div class="card-list">
<div
@ -251,7 +253,7 @@ const hasVisibleItems = computed(() =>
</div>
<div class="card-info">
<p class="card-name">{{ item.item_name }}</p>
<span class="badge-avail">Favorito</span>
<span class="badge-avail">{{ t('favorites.saved') }}</span>
</div>
<button class="heart-btn heart-btn--active" @click.stop="removeFavorite($event, item.item_type, item.item_id)">
<span class="material-icons">favorite</span>
@ -268,7 +270,7 @@ const hasVisibleItems = computed(() =>
<style scoped>
/* ══════════════════════════════════════════
PÁGINA BASE
══════════════════════════════════════════ */
══════════════════════════════════════════ */
.fav-page {
min-height: 100vh;
background: var(--bg-primary);
@ -277,7 +279,7 @@ const hasVisibleItems = computed(() =>
/* ══════════════════════════════════════════
HEADER
══════════════════════════════════════════ */
══════════════════════════════════════════ */
.fav-header {
padding: 1.5rem 1.25rem 1rem;
background: var(--bg-secondary);
@ -301,7 +303,7 @@ const hasVisibleItems = computed(() =>
/* ══════════════════════════════════════════
CHIPS
══════════════════════════════════════════ */
══════════════════════════════════════════ */
.chips-wrap {
background: var(--bg-secondary);
padding: 0.75rem 0 0.75rem;
@ -355,7 +357,7 @@ const hasVisibleItems = computed(() =>
/* ══════════════════════════════════════════
CONTENIDO
══════════════════════════════════════════ */
══════════════════════════════════════════ */
.fav-content {
padding: 1.25rem;
display: flex;
@ -385,7 +387,7 @@ const hasVisibleItems = computed(() =>
/* ══════════════════════════════════════════
TARJETA FILA (rutas / taxis / eventos)
══════════════════════════════════════════ */
══════════════════════════════════════════ */
.card-list { display: flex; flex-direction: column; gap: 0.625rem; }
.card {
@ -440,6 +442,13 @@ const hasVisibleItems = computed(() =>
.card-thumb--event .material-icons { font-size: 1.5rem; }
.card-thumb--blue {
background: #3b82f6;
color: white;
}
.card-thumb--blue .material-icons { font-size: 1.5rem; }
/* Info */
.card-info { flex: 1; min-width: 0; }
@ -475,7 +484,7 @@ const hasVisibleItems = computed(() =>
/* ══════════════════════════════════════════
BOTÓN CORAZÓN
══════════════════════════════════════════ */
══════════════════════════════════════════ */
.heart-btn {
width: 38px;
height: 38px;
@ -522,7 +531,7 @@ const hasVisibleItems = computed(() =>
/* ══════════════════════════════════════════
GRID DE NEGOCIOS
══════════════════════════════════════════ */
══════════════════════════════════════════ */
.biz-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
@ -584,7 +593,7 @@ const hasVisibleItems = computed(() =>
/* ══════════════════════════════════════════
ESTADOS
══════════════════════════════════════════ */
══════════════════════════════════════════ */
.state-center {
display: flex;
flex-direction: column;
@ -676,7 +685,7 @@ const hasVisibleItems = computed(() =>
/* ══════════════════════════════════════════
RESPONSIVE
══════════════════════════════════════════ */
══════════════════════════════════════════ */
@media (max-width: 480px) {
.biz-grid {
grid-template-columns: repeat(2, 1fr);
@ -689,4 +698,3 @@ const hasVisibleItems = computed(() =>
.biz-grid { grid-template-columns: repeat(3, 1fr); }
}
</style>