Fix: Map offers button design, bottom nav cleanup, sidebar theme toggle simplification, and tourist trip auto-scroll animation

This commit is contained in:
2026-02-24 13:02:19 -05:00
parent 6d4f50cafb
commit c4046541a5
6 changed files with 126 additions and 70 deletions

View File

@ -60,14 +60,10 @@
</div>
</div>
<div class="sidebar-group">
<div class="group-label">APARIENCIA</div>
<div class="sidebar-link theme-toggle-row">
<span class="material-icons">dark_mode</span>
<span class="link-text">Modo Visual</span>
<ThemeToggle class="sidebar-theme-switch" />
<div class="sidebar-link theme-toggle-row" @click="themeStore.toggleDarkMode">
<span class="material-icons">{{ themeStore.isDarkMode ? 'light_mode' : 'dark_mode' }}</span>
<span class="link-text">{{ themeStore.isDarkMode ? 'Modo Claro' : 'Modo Oscuro' }}</span>
</div>
</div>
<div class="sidebar-group">
<div class="group-label">SOPORTE</div>
@ -104,11 +100,12 @@ import { ref, onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useThemeStore } from '@/stores/theme'
import ReportModal from './ReportModal.vue'
import ThemeToggle from './common/ThemeToggle.vue'
const { t, locale } = useI18n()
const authStore = useAuthStore()
const themeStore = useThemeStore()
const router = useRouter()
const showMenu = ref(false)
const showReportModal = ref(false)

View File

@ -11,7 +11,7 @@ const navItems = [
{ name: 'map', path: '/map', icon: 'map' },
{ name: 'schedules', path: '/schedules', icon: 'schedule' },
{ name: 'discover', path: '/discover', icon: 'explore' },
{ name: 'taxi', path: '/taxi', icon: 'directions_bus' } // Cambiado a ícono de transporte más general
{ name: 'taxi', path: '/taxi', icon: 'directions_bus' }
]
const navigateTo = (path: string) => {

View File

@ -10,6 +10,7 @@ import { analyticsService } from '@/services/analyticsService'
const router = useRouter()
const businesses = ref<Business[]>([])
const isLoading = ref(true)
const error = ref<string | null>(null)
const searchQuery = ref('')
const selectedCategory = ref('Todas')
const selectedArea = ref('Todas')
@ -32,15 +33,22 @@ function catIcon(cat: string) {
return CATEGORY_META[cat]?.icon ?? 'place'
}
onMounted(async () => {
analyticsService.logEvent({ event_name: 'screen_view', screen_name: 'Discover' })
async function loadBusinesses() {
isLoading.value = true
error.value = null
try {
businesses.value = await businessService.getAllBusinesses()
} catch (e) {
console.error('Error loading businesses:', e)
error.value = 'No se pudieron cargar los lugares. Revisa tu conexión.'
} finally {
isLoading.value = false
}
}
onMounted(() => {
analyticsService.logEvent({ event_name: 'screen_view', screen_name: 'Discover' })
loadBusinesses()
})
// ── Computados
@ -149,6 +157,16 @@ function resetFilters() {
<p>Cargando lugares...</p>
</div>
<!-- ERROR -->
<div v-else-if="error" class="state-center">
<span class="material-icons" style="font-size: 3.5rem; color: #ef4444; opacity: 0.8; margin-bottom: 0.5rem;">error_outline</span>
<p style="font-weight: 600; color: var(--text-secondary);">{{ error }}</p>
<button class="cta-btn" style="margin-top: 1rem;" @click="loadBusinesses">
<span class="material-icons">refresh</span>
Reintentar
</button>
</div>
<template v-else>
<!-- VISTA CON FILTRO ACTIVO -->

View File

@ -921,18 +921,19 @@ function clearNavigation() {
<!-- Floating UI Elements -->
<div class="map-floating-controls">
<!-- Promos FAB Button -->
<!-- Botón de Ofertas (FAB Simple) -->
<button
v-if="isLoaded && couponStore.coupons.length > 0"
class="offers-fab"
:class="{ 'offers-fab--open': showPromos }"
v-if="isLoaded"
class="offers-fab pulse"
:class="{ 'active': showPromos }"
@click="showPromos = !showPromos"
title="Ver Ofertas"
>
<span class="material-icons offers-fab-icon">
<span class="material-icons">
{{ showPromos ? 'close' : 'local_offer' }}
</span>
<span v-if="!showPromos" class="offers-fab-badge">{{ couponStore.coupons.length }}</span>
<span v-if="couponStore.coupons.length > 0 && !showPromos" class="offers-badge">
{{ couponStore.coupons.length }}
</span>
</button>
<!-- Location Button (Animated Pin) -->
@ -1105,14 +1106,14 @@ function clearNavigation() {
<!-- Handle -->
<div class="sheet-handle"></div>
<!-- Header -->
<!-- Cabecera -->
<div class="sheet-header">
<div class="sheet-header-left">
<span class="material-icons sheet-star">stars</span>
<span class="sheet-title">Ofertas SIBU</span>
<span class="sheet-count-badge">{{ couponStore.coupons.length }} disponibles</span>
<div class="sheet-title-group">
<span class="material-icons">local_offer</span>
<strong>Ofertas Disponibles</strong>
<span class="sheet-count">({{ couponStore.coupons.length }})</span>
</div>
<button class="sheet-close" @click="showPromos = false">
<button class="close-btn" @click="showPromos = false">
<span class="material-icons">close</span>
</button>
</div>
@ -1239,54 +1240,42 @@ function clearNavigation() {
}
/* ═══════════════════════════════════════
OFFERS FAB BUTTON
BOTÓN DE OFERTAS (MAPA)
Mantenido simple y funcional
No premiun - solo funcional
═══════════════════════════════════════ */
.offers-fab {
position: relative;
width: 52px;
height: 52px;
width: 56px;
height: 56px;
border-radius: 50%;
background: var(--active-color);
color: #101820;
background: #fee715;
color: #000;
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 20px rgba(254, 231, 21, 0.45);
transition: transform 0.2s, box-shadow 0.2s;
flex-shrink: 0;
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
position: relative;
z-index: 1001;
}
.offers-fab:hover { transform: scale(1.08); }
.offers-fab:active { transform: scale(0.96); }
.offers-fab--open {
background: var(--bg-secondary);
color: var(--text-primary);
box-shadow: 0 4px 16px rgba(0,0,0,0.25);
border: 1px solid var(--border-color);
.offers-fab.active {
background: #f44336;
color: #fff;
}
.offers-fab-icon { font-size: 1.5rem; transition: color 0.2s; }
.offers-fab-badge {
.offers-badge {
position: absolute;
top: -4px;
right: -4px;
min-width: 18px;
height: 18px;
padding: 0 4px;
background: #ef4444;
color: #ffffff;
font-size: 0.625rem;
font-weight: 800;
border-radius: 99px;
border: 2px solid var(--bg-primary);
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
top: -5px;
right: -5px;
background: #f44336;
color: white;
font-size: 12px;
font-weight: bold;
padding: 2px 6px;
border-radius: 10px;
border: 2px solid #fff;
}
/* ═══════════════════════════════════════
@ -1294,15 +1283,24 @@ function clearNavigation() {
═══════════════════════════════════════ */
.offers-sheet {
position: fixed;
bottom: calc(64px + env(safe-area-inset-bottom, 0px)); /* above bottom nav + safe area */
left: 0;
right: 0;
background: var(--bg-secondary);
border-top: 1px solid var(--border-color);
border-radius: 20px 20px 0 0;
z-index: 1300;
padding: 0 0 0.75rem;
box-shadow: 0 -8px 32px rgba(0,0,0,0.25);
bottom: 110px; /* Separado más de la barra inferior para evitar solapamiento */
left: 10px;
right: 10px;
background: #fff;
border: 2px solid #000;
border-radius: 12px;
z-index: 2000;
padding-bottom: 10px;
box-shadow: 0 -4px 15px rgba(0,0,0,0.2);
color: #000;
}
@media (prefers-color-scheme: dark) {
.offers-sheet {
background: #111;
color: #fff;
border-color: #333;
}
}
.sheet-handle {

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
import { onMounted, ref, computed, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { useTaxiStore } from '@/stores/taxi'
import { useShuttleStore } from '@/stores/shuttle'
@ -24,6 +24,24 @@ const shifts = ['all', 'dia', 'tarde', 'noche']
const shuttleRouteFilter = ref('all')
const shuttleTypeFilter = ref('all')
const expandedShuttleId = ref<string | null>(null)
const shuttleRefs = ref<Record<string, any>>({})
const setShuttleRef = (el: any, id: string) => {
if (el) shuttleRefs.value[id] = el
}
watch(expandedShuttleId, async (newVal) => {
if (newVal) {
await nextTick()
const el = shuttleRefs.value[newVal]
if (el) {
// Small timeout to wait for the CSS height transition if any
setTimeout(() => {
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
}, 100)
}
}
})
const shuttleRoutes = computed(() => {
const routes = shuttleStore.shuttles.map(s => `${s.origin} - ${s.destination}`)
@ -167,6 +185,10 @@ function getShiftLabel(shift: string) {
<div v-else-if="taxiStore.error" class="state-container">
<span class="material-icons">error_outline</span>
<p>{{ taxiStore.error }}</p>
<button class="retry-btn" @click="taxiStore.loadTaxis()">
<span class="material-icons">refresh</span>
Reintentar
</button>
</div>
<div v-else class="taxis-grid">
@ -263,6 +285,7 @@ function getShiftLabel(shift: string) {
<div
v-for="shuttle in filteredShuttles"
:key="shuttle.id"
:ref="el => setShuttleRef(el, shuttle.id)"
class="shuttle-card"
:class="{ expanded: expandedShuttleId === shuttle.id }"
:style="{ backgroundImage: `url(${shuttle.image_url || 'https://images.unsplash.com/photo-1544620347-c4fd4a3d5957?auto=format&fit=crop&q=80&w=2069'})` }"
@ -431,6 +454,26 @@ function getShiftLabel(shift: string) {
z-index: 1;
box-shadow: 0 4px 15px rgba(254, 231, 21, 0.4);
}
.retry-btn {
margin-top: 1rem;
padding: 10px 20px;
background: var(--active-color);
color: #101820;
border: none;
border-radius: 12px;
font-weight: 800;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: transform 0.2s;
}
.retry-btn:active {
transform: scale(0.95);
}
.retry-btn .material-icons {
font-size: 18px;
}
/* =============================================
SHUTTLE CARDS — DISEÑO PREMIUM CON FOTO