Fix: Map offers button design, bottom nav cleanup, sidebar theme toggle simplification, and tourist trip auto-scroll animation
This commit is contained in:
@ -1,2 +1,2 @@
|
|||||||
VITE_GOOGLE_MAPS_API_KEY=AIzaSyDrKzxJ-9A48cWFRffpOnEdmRR1Ijjj--Y
|
VITE_GOOGLE_MAPS_API_KEY=AIzaSyDrKzxJ-9A48cWFRffpOnEdmRR1Ijjj--Y
|
||||||
VITE_API_URL=https://sibu2-0.onrender.com
|
VITE_API_URL=https://sibu-backend.onrender.com
|
||||||
|
|||||||
@ -60,13 +60,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-group">
|
<div class="sidebar-link theme-toggle-row" @click="themeStore.toggleDarkMode">
|
||||||
<div class="group-label">APARIENCIA</div>
|
<span class="material-icons">{{ themeStore.isDarkMode ? 'light_mode' : 'dark_mode' }}</span>
|
||||||
<div class="sidebar-link theme-toggle-row">
|
<span class="link-text">{{ themeStore.isDarkMode ? 'Modo Claro' : 'Modo Oscuro' }}</span>
|
||||||
<span class="material-icons">dark_mode</span>
|
|
||||||
<span class="link-text">Modo Visual</span>
|
|
||||||
<ThemeToggle class="sidebar-theme-switch" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-group">
|
<div class="sidebar-group">
|
||||||
@ -104,11 +100,12 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
import ReportModal from './ReportModal.vue'
|
import ReportModal from './ReportModal.vue'
|
||||||
import ThemeToggle from './common/ThemeToggle.vue'
|
|
||||||
|
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
const themeStore = useThemeStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const showMenu = ref(false)
|
const showMenu = ref(false)
|
||||||
const showReportModal = ref(false)
|
const showReportModal = ref(false)
|
||||||
|
|||||||
@ -11,7 +11,7 @@ const navItems = [
|
|||||||
{ name: 'map', path: '/map', icon: 'map' },
|
{ name: 'map', path: '/map', icon: 'map' },
|
||||||
{ name: 'schedules', path: '/schedules', icon: 'schedule' },
|
{ name: 'schedules', path: '/schedules', icon: 'schedule' },
|
||||||
{ name: 'discover', path: '/discover', icon: 'explore' },
|
{ 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) => {
|
const navigateTo = (path: string) => {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { analyticsService } from '@/services/analyticsService'
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const businesses = ref<Business[]>([])
|
const businesses = ref<Business[]>([])
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const selectedCategory = ref('Todas')
|
const selectedCategory = ref('Todas')
|
||||||
const selectedArea = ref('Todas')
|
const selectedArea = ref('Todas')
|
||||||
@ -32,15 +33,22 @@ function catIcon(cat: string) {
|
|||||||
return CATEGORY_META[cat]?.icon ?? 'place'
|
return CATEGORY_META[cat]?.icon ?? 'place'
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
async function loadBusinesses() {
|
||||||
analyticsService.logEvent({ event_name: 'screen_view', screen_name: 'Discover' })
|
isLoading.value = true
|
||||||
|
error.value = null
|
||||||
try {
|
try {
|
||||||
businesses.value = await businessService.getAllBusinesses()
|
businesses.value = await businessService.getAllBusinesses()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error loading businesses:', e)
|
console.error('Error loading businesses:', e)
|
||||||
|
error.value = 'No se pudieron cargar los lugares. Revisa tu conexión.'
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
analyticsService.logEvent({ event_name: 'screen_view', screen_name: 'Discover' })
|
||||||
|
loadBusinesses()
|
||||||
})
|
})
|
||||||
|
|
||||||
// ── Computados
|
// ── Computados
|
||||||
@ -149,6 +157,16 @@ function resetFilters() {
|
|||||||
<p>Cargando lugares...</p>
|
<p>Cargando lugares...</p>
|
||||||
</div>
|
</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>
|
<template v-else>
|
||||||
|
|
||||||
<!-- ══ VISTA CON FILTRO ACTIVO ══ -->
|
<!-- ══ VISTA CON FILTRO ACTIVO ══ -->
|
||||||
|
|||||||
@ -921,18 +921,19 @@ function clearNavigation() {
|
|||||||
|
|
||||||
<!-- Floating UI Elements -->
|
<!-- Floating UI Elements -->
|
||||||
<div class="map-floating-controls">
|
<div class="map-floating-controls">
|
||||||
<!-- Promos FAB Button -->
|
<!-- Botón de Ofertas (FAB Simple) -->
|
||||||
<button
|
<button
|
||||||
v-if="isLoaded && couponStore.coupons.length > 0"
|
v-if="isLoaded"
|
||||||
class="offers-fab"
|
class="offers-fab pulse"
|
||||||
:class="{ 'offers-fab--open': showPromos }"
|
:class="{ 'active': showPromos }"
|
||||||
@click="showPromos = !showPromos"
|
@click="showPromos = !showPromos"
|
||||||
title="Ver Ofertas"
|
|
||||||
>
|
>
|
||||||
<span class="material-icons offers-fab-icon">
|
<span class="material-icons">
|
||||||
{{ showPromos ? 'close' : 'local_offer' }}
|
{{ showPromos ? 'close' : 'local_offer' }}
|
||||||
</span>
|
</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>
|
</button>
|
||||||
|
|
||||||
<!-- Location Button (Animated Pin) -->
|
<!-- Location Button (Animated Pin) -->
|
||||||
@ -1105,14 +1106,14 @@ function clearNavigation() {
|
|||||||
<!-- Handle -->
|
<!-- Handle -->
|
||||||
<div class="sheet-handle"></div>
|
<div class="sheet-handle"></div>
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Cabecera -->
|
||||||
<div class="sheet-header">
|
<div class="sheet-header">
|
||||||
<div class="sheet-header-left">
|
<div class="sheet-title-group">
|
||||||
<span class="material-icons sheet-star">stars</span>
|
<span class="material-icons">local_offer</span>
|
||||||
<span class="sheet-title">Ofertas SIBU</span>
|
<strong>Ofertas Disponibles</strong>
|
||||||
<span class="sheet-count-badge">{{ couponStore.coupons.length }} disponibles</span>
|
<span class="sheet-count">({{ couponStore.coupons.length }})</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="sheet-close" @click="showPromos = false">
|
<button class="close-btn" @click="showPromos = false">
|
||||||
<span class="material-icons">close</span>
|
<span class="material-icons">close</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 {
|
.offers-fab {
|
||||||
position: relative;
|
width: 56px;
|
||||||
width: 52px;
|
height: 56px;
|
||||||
height: 52px;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--active-color);
|
background: #fee715;
|
||||||
color: #101820;
|
color: #000;
|
||||||
border: none;
|
border: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 4px 20px rgba(254, 231, 21, 0.45);
|
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
position: relative;
|
||||||
flex-shrink: 0;
|
z-index: 1001;
|
||||||
}
|
}
|
||||||
|
|
||||||
.offers-fab:hover { transform: scale(1.08); }
|
.offers-fab.active {
|
||||||
.offers-fab:active { transform: scale(0.96); }
|
background: #f44336;
|
||||||
|
color: #fff;
|
||||||
.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-icon { font-size: 1.5rem; transition: color 0.2s; }
|
.offers-badge {
|
||||||
|
|
||||||
.offers-fab-badge {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -4px;
|
top: -5px;
|
||||||
right: -4px;
|
right: -5px;
|
||||||
min-width: 18px;
|
background: #f44336;
|
||||||
height: 18px;
|
color: white;
|
||||||
padding: 0 4px;
|
font-size: 12px;
|
||||||
background: #ef4444;
|
font-weight: bold;
|
||||||
color: #ffffff;
|
padding: 2px 6px;
|
||||||
font-size: 0.625rem;
|
border-radius: 10px;
|
||||||
font-weight: 800;
|
border: 2px solid #fff;
|
||||||
border-radius: 99px;
|
|
||||||
border: 2px solid var(--bg-primary);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ═══════════════════════════════════════
|
/* ═══════════════════════════════════════
|
||||||
@ -1294,15 +1283,24 @@ function clearNavigation() {
|
|||||||
═══════════════════════════════════════ */
|
═══════════════════════════════════════ */
|
||||||
.offers-sheet {
|
.offers-sheet {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: calc(64px + env(safe-area-inset-bottom, 0px)); /* above bottom nav + safe area */
|
bottom: 110px; /* Separado más de la barra inferior para evitar solapamiento */
|
||||||
left: 0;
|
left: 10px;
|
||||||
right: 0;
|
right: 10px;
|
||||||
background: var(--bg-secondary);
|
background: #fff;
|
||||||
border-top: 1px solid var(--border-color);
|
border: 2px solid #000;
|
||||||
border-radius: 20px 20px 0 0;
|
border-radius: 12px;
|
||||||
z-index: 1300;
|
z-index: 2000;
|
||||||
padding: 0 0 0.75rem;
|
padding-bottom: 10px;
|
||||||
box-shadow: 0 -8px 32px rgba(0,0,0,0.25);
|
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 {
|
.sheet-handle {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, computed } from 'vue'
|
import { onMounted, ref, computed, watch, nextTick } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useTaxiStore } from '@/stores/taxi'
|
import { useTaxiStore } from '@/stores/taxi'
|
||||||
import { useShuttleStore } from '@/stores/shuttle'
|
import { useShuttleStore } from '@/stores/shuttle'
|
||||||
@ -24,6 +24,24 @@ const shifts = ['all', 'dia', 'tarde', 'noche']
|
|||||||
const shuttleRouteFilter = ref('all')
|
const shuttleRouteFilter = ref('all')
|
||||||
const shuttleTypeFilter = ref('all')
|
const shuttleTypeFilter = ref('all')
|
||||||
const expandedShuttleId = ref<string | null>(null)
|
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 shuttleRoutes = computed(() => {
|
||||||
const routes = shuttleStore.shuttles.map(s => `${s.origin} - ${s.destination}`)
|
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">
|
<div v-else-if="taxiStore.error" class="state-container">
|
||||||
<span class="material-icons">error_outline</span>
|
<span class="material-icons">error_outline</span>
|
||||||
<p>{{ taxiStore.error }}</p>
|
<p>{{ taxiStore.error }}</p>
|
||||||
|
<button class="retry-btn" @click="taxiStore.loadTaxis()">
|
||||||
|
<span class="material-icons">refresh</span>
|
||||||
|
Reintentar
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="taxis-grid">
|
<div v-else class="taxis-grid">
|
||||||
@ -263,6 +285,7 @@ function getShiftLabel(shift: string) {
|
|||||||
<div
|
<div
|
||||||
v-for="shuttle in filteredShuttles"
|
v-for="shuttle in filteredShuttles"
|
||||||
:key="shuttle.id"
|
:key="shuttle.id"
|
||||||
|
:ref="el => setShuttleRef(el, shuttle.id)"
|
||||||
class="shuttle-card"
|
class="shuttle-card"
|
||||||
:class="{ expanded: expandedShuttleId === shuttle.id }"
|
: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'})` }"
|
: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;
|
z-index: 1;
|
||||||
box-shadow: 0 4px 15px rgba(254, 231, 21, 0.4);
|
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
|
SHUTTLE CARDS — DISEÑO PREMIUM CON FOTO
|
||||||
|
|||||||
Reference in New Issue
Block a user