Fix: Map offers button design, bottom nav cleanup, sidebar theme toggle simplification, and tourist trip auto-scroll animation
This commit is contained in:
@ -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)
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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 ══ -->
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user