fix: critical bug fixes - routes UUID, image paths, favorites loading, bottom nav debounce
This commit is contained in:
@ -4,12 +4,16 @@ import { RouterView, useRoute } from "vue-router";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import MainLayout from "./components/layouts/MainLayout.vue";
|
||||
import { useThemeStore } from './stores/theme'
|
||||
import { useAuthStore } from './stores/auth'
|
||||
import { useFavoritesStore } from './stores/favorites'
|
||||
import { analyticsService } from '@/services/analyticsService'
|
||||
|
||||
// Initialize theme store
|
||||
const route = useRoute()
|
||||
const { locale } = useI18n()
|
||||
const themeStore = useThemeStore()
|
||||
const authStore = useAuthStore()
|
||||
const favoritesStore = useFavoritesStore()
|
||||
|
||||
const isSplashScreen = computed(() => route.name === 'splash')
|
||||
const isAuthScreen = computed(() => route.name === 'auth' || route.path === '/login')
|
||||
@ -20,6 +24,10 @@ onMounted(() => {
|
||||
event_name: 'app_open',
|
||||
properties: { language: locale.value }
|
||||
})
|
||||
// Load favorites if the user is already logged in
|
||||
if (authStore.isAuthenticated) {
|
||||
favoritesStore.loadFavorites()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -14,12 +14,20 @@ const navItems = [
|
||||
{ name: 'taxi', path: '/taxi', icon: 'directions_bus' }
|
||||
]
|
||||
|
||||
let isNavigating = false
|
||||
|
||||
const navigateTo = (path: string) => {
|
||||
router.push(path)
|
||||
// Prevent rapid multiple navigations (debounce guard)
|
||||
if (isNavigating) return
|
||||
if (route.path === path) return
|
||||
isNavigating = true
|
||||
router.push(path).finally(() => {
|
||||
setTimeout(() => { isNavigating = false }, 300)
|
||||
})
|
||||
}
|
||||
|
||||
const isActive = (path: string) => {
|
||||
return route.path === path
|
||||
return route.path === path || route.path.startsWith(path + '/')
|
||||
}
|
||||
|
||||
// Scroll detection logic
|
||||
|
||||
38
frontend/src/utils/imageUrl.ts
Normal file
38
frontend/src/utils/imageUrl.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { API_URL } from '@/services/apiClient'
|
||||
|
||||
/**
|
||||
* Returns a full URL for an image path.
|
||||
* Handles null/undefined paths, absolute URLs, and relative backend paths.
|
||||
*/
|
||||
export function getImageUrl(path?: string | null, type: 'taxi' | 'shuttle' | 'business' | 'coupon' = 'business') {
|
||||
if (!path) {
|
||||
const defaultNames: Record<string, string> = {
|
||||
taxi: 'Taxi',
|
||||
shuttle: 'Transporte',
|
||||
business: 'Negocio',
|
||||
coupon: 'Oferta'
|
||||
}
|
||||
const name = defaultNames[type] || 'SIBU'
|
||||
return `https://ui-avatars.com/api/?name=${name}&background=fee715&color=101820&size=256&bold=true`
|
||||
}
|
||||
|
||||
if (path.startsWith('http')) {
|
||||
return path
|
||||
}
|
||||
|
||||
// Ensure path starts with / for joining with API_URL
|
||||
const cleanPath = path.startsWith('/') ? path : `/${path}`
|
||||
|
||||
// Ensure API_URL includes protocol if missing (safety check)
|
||||
let cleanBaseUrl = API_URL.trim()
|
||||
if (!cleanBaseUrl.startsWith('http')) {
|
||||
cleanBaseUrl = `https://${cleanBaseUrl}`
|
||||
}
|
||||
|
||||
// Remove trailing slash from base URL
|
||||
if (cleanBaseUrl.endsWith('/')) {
|
||||
cleanBaseUrl = cleanBaseUrl.slice(0, -1)
|
||||
}
|
||||
|
||||
return `${cleanBaseUrl}${cleanPath}`
|
||||
}
|
||||
@ -211,7 +211,8 @@ async function saveShuttle() {
|
||||
|
||||
<div class="preview-container">
|
||||
<!-- PREVIEW CARD -->
|
||||
<div class="shuttle-card-preview" :class="{ expanded: true }" :style="{ backgroundImage: `url(${previewImageUrl})` }">
|
||||
<div class="shuttle-card-preview" :class="{ expanded: true }">
|
||||
<img :src="previewImageUrl" class="shuttle-card-bg" @error="(e) => (e.target as HTMLImageElement).src = 'https://images.unsplash.com/photo-1449034446853-66c86144b0ad?q=80&w=2070&auto=format&fit=crop'" />
|
||||
<div class="shuttle-main-info">
|
||||
<div class="shuttle-header-mini">
|
||||
<div class="company-badge">
|
||||
@ -516,12 +517,11 @@ async function saveShuttle() {
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255,255,255,0.12);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 30px 60px rgba(0,0,0,0.5);
|
||||
background: #101820;
|
||||
}
|
||||
|
||||
.shuttle-card-preview::before {
|
||||
@ -534,7 +534,7 @@ async function saveShuttle() {
|
||||
rgba(0, 0, 0, 0.65) 55%,
|
||||
rgba(0, 0, 0, 0.30) 100%
|
||||
);
|
||||
z-index: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.shuttle-card-preview.expanded {
|
||||
@ -553,7 +553,7 @@ async function saveShuttle() {
|
||||
.shuttle-main-info,
|
||||
.shuttle-details {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { businessService } from '@/services/businessService'
|
||||
import { API_URL } from '@/services/apiClient'
|
||||
import type { Business } from '@/types'
|
||||
import { useRouter } from 'vue-router'
|
||||
import FavoriteButton from '@/components/FavoriteButton.vue'
|
||||
import { analyticsService } from '@/services/analyticsService'
|
||||
import { getImageUrl } from '@/utils/imageUrl'
|
||||
|
||||
const router = useRouter()
|
||||
const businesses = ref<Business[]>([])
|
||||
@ -98,11 +98,6 @@ function handleExplore(biz: Business) {
|
||||
router.push('/business/' + biz.id)
|
||||
}
|
||||
|
||||
function getImageUrl(path?: string | null) {
|
||||
if (!path) return `https://ui-avatars.com/api/?name=Negocio&background=fee715&color=101820&size=200&bold=true`
|
||||
if (path.startsWith('http')) return path
|
||||
return `${API_URL}${path.startsWith('/') ? '' : '/'}${path}`
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
selectedCategory.value = 'Todas'
|
||||
@ -209,7 +204,12 @@ function resetFilters() {
|
||||
@click="handleExplore(biz)"
|
||||
>
|
||||
<div class="biz-img-wrap">
|
||||
<img :src="getImageUrl(biz.image_url)" :alt="biz.name" class="biz-img" />
|
||||
<img
|
||||
:src="getImageUrl(biz.image_url, 'business')"
|
||||
:alt="biz.name"
|
||||
class="biz-img"
|
||||
@error="(e) => (e.target as HTMLImageElement).src = getImageUrl(null, 'business')"
|
||||
/>
|
||||
<div class="biz-fav">
|
||||
<FavoriteButton item-type="business" :item-id="biz.id" :item-name="biz.name" :item-image="biz.image_url || undefined" />
|
||||
</div>
|
||||
@ -274,7 +274,12 @@ function resetFilters() {
|
||||
class="featured-card"
|
||||
@click="handleExplore(biz)"
|
||||
>
|
||||
<img :src="getImageUrl(biz.image_url)" :alt="biz.name" class="featured-img" />
|
||||
<img
|
||||
:src="getImageUrl(biz.image_url, 'business')"
|
||||
:alt="biz.name"
|
||||
class="featured-img"
|
||||
@error="(e) => (e.target as HTMLImageElement).src = getImageUrl(null, 'business')"
|
||||
/>
|
||||
<div class="featured-gradient"></div>
|
||||
<div class="featured-fav">
|
||||
<FavoriteButton item-type="business" :item-id="biz.id" :item-name="biz.name" :item-image="biz.image_url || undefined" />
|
||||
@ -305,7 +310,12 @@ function resetFilters() {
|
||||
@click="handleExplore(biz)"
|
||||
>
|
||||
<div class="biz-img-wrap">
|
||||
<img :src="getImageUrl(biz.image_url)" :alt="biz.name" class="biz-img" />
|
||||
<img
|
||||
:src="getImageUrl(biz.image_url, 'business')"
|
||||
:alt="biz.name"
|
||||
class="biz-img"
|
||||
@error="(e) => (e.target as HTMLImageElement).src = getImageUrl(null, 'business')"
|
||||
/>
|
||||
<div class="biz-fav">
|
||||
<FavoriteButton item-type="business" :item-id="biz.id" :item-name="biz.name" :item-image="biz.image_url || undefined" />
|
||||
</div>
|
||||
|
||||
@ -7,9 +7,9 @@ import { useMapStore } from "@/stores/map";
|
||||
import { useBusStopStore } from "@/stores/busStop";
|
||||
import { useCouponStore } from "@/stores/coupon";
|
||||
import { useGoogleMaps } from "@/composables/useGoogleMaps";
|
||||
import { API_URL } from "@/services/apiClient";
|
||||
import { telemetryService } from "@/services/telemetryService";
|
||||
import { analyticsService } from "@/services/analyticsService";
|
||||
import { getImageUrl } from "@/utils/imageUrl";
|
||||
|
||||
import BusStopInfoModal from "@/components/BusStopInfoModal.vue";
|
||||
import type { BusStop } from '@/types'
|
||||
@ -194,11 +194,6 @@ function closePromoModal() {
|
||||
}
|
||||
|
||||
|
||||
function getImageUrl(path: string | null | undefined) {
|
||||
if (!path) return '/default-coupon.png'
|
||||
if (path.startsWith('http')) return path
|
||||
return `${API_URL}${path.startsWith('/') ? '' : '/'}${path}`
|
||||
}
|
||||
|
||||
async function claimPromo() {
|
||||
if (!selectedPromo.value) return
|
||||
@ -1150,7 +1145,12 @@ function clearNavigation() {
|
||||
>
|
||||
<!-- Image -->
|
||||
<div class="sheet-img-wrap">
|
||||
<img :src="getImageUrl(currentPromo.image_url)" class="sheet-img" :alt="currentPromo.title" />
|
||||
<img
|
||||
:src="getImageUrl(currentPromo.image_url, 'coupon')"
|
||||
class="sheet-img"
|
||||
:alt="currentPromo.title"
|
||||
@error="(e) => (e.target as HTMLImageElement).src = getImageUrl(null, 'coupon')"
|
||||
/>
|
||||
<span v-if="currentPromo.discount_percentage" class="sheet-discount">
|
||||
-{{ currentPromo.discount_percentage }}%
|
||||
</span>
|
||||
@ -1203,7 +1203,11 @@ function clearNavigation() {
|
||||
<div v-if="showPromoModal && selectedPromo" class="promo-modal-overlay" @click="closePromoModal">
|
||||
<div class="promo-modal-content" @click.stop>
|
||||
<div class="promo-header-modal">
|
||||
<img :src="getImageUrl(selectedPromo.image_url)" class="promo-img-modal" />
|
||||
<img
|
||||
:src="getImageUrl(selectedPromo.image_url, 'coupon')"
|
||||
class="promo-img-modal"
|
||||
@error="(e) => (e.target as HTMLImageElement).src = getImageUrl(null, 'coupon')"
|
||||
/>
|
||||
<div class="promo-badge-modal">PROMO</div>
|
||||
</div>
|
||||
<div class="promo-body-modal">
|
||||
|
||||
@ -4,9 +4,9 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useTaxiStore } from '@/stores/taxi'
|
||||
import { useShuttleStore } from '@/stores/shuttle'
|
||||
import { analyticsService } from '@/services/analyticsService'
|
||||
import { API_URL } from '@/services/apiClient'
|
||||
import type { Taxi, Shuttle } from '@/types'
|
||||
import FavoriteButton from '@/components/FavoriteButton.vue'
|
||||
import { getImageUrl } from '@/utils/imageUrl'
|
||||
|
||||
const { t } = useI18n()
|
||||
const taxiStore = useTaxiStore()
|
||||
@ -74,11 +74,6 @@ const filteredTaxis = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
function getImageUrl(path?: string) {
|
||||
if (!path) return `https://ui-avatars.com/api/?name=Taxi&background=fee715&color=101820`
|
||||
if (path.startsWith('http')) return path
|
||||
return `${API_URL}${path.startsWith('/') ? '' : '/'}${path}`
|
||||
}
|
||||
|
||||
const handleCall = (taxi: Taxi) => {
|
||||
analyticsService.logEvent({
|
||||
@ -195,7 +190,11 @@ function getShiftLabel(shift: string) {
|
||||
<div v-for="taxi in filteredTaxis" :key="taxi.id" class="taxi-card-new">
|
||||
<div class="card-top">
|
||||
<div class="driver-avatar">
|
||||
<img :src="getImageUrl(taxi.image_url)" alt="Driver">
|
||||
<img
|
||||
:src="getImageUrl(taxi.image_url, 'taxi')"
|
||||
alt="Driver"
|
||||
@error="(e) => (e.target as HTMLImageElement).src = getImageUrl(null, 'taxi')"
|
||||
>
|
||||
</div>
|
||||
<div class="driver-info">
|
||||
<h3>{{ taxi.owner_name }}</h3>
|
||||
@ -288,7 +287,6 @@ function getShiftLabel(shift: string) {
|
||||
:ref="el => setShuttleRef(el, shuttle.id)"
|
||||
class="shuttle-card"
|
||||
:class="{ expanded: expandedShuttleId === shuttle.id }"
|
||||
:style="{ backgroundImage: `url(${getImageUrl(shuttle.image_url)})` }"
|
||||
@click="() => {
|
||||
expandedShuttleId = expandedShuttleId === shuttle.id ? null : shuttle.id;
|
||||
if (expandedShuttleId === shuttle.id) {
|
||||
@ -296,6 +294,11 @@ function getShiftLabel(shift: string) {
|
||||
}
|
||||
}"
|
||||
>
|
||||
<img
|
||||
:src="getImageUrl(shuttle.image_url, 'shuttle')"
|
||||
class="shuttle-card-bg"
|
||||
@error="(e) => (e.target as HTMLImageElement).src = getImageUrl(null, 'shuttle')"
|
||||
/>
|
||||
<!-- Collapsed info (always visible) -->
|
||||
<div class="shuttle-main-info">
|
||||
<div class="shuttle-header-mini">
|
||||
@ -502,16 +505,24 @@ function getShiftLabel(shift: string) {
|
||||
/* ---- La tarjeta base ---- */
|
||||
.shuttle-card {
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255,255,255,0.12);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
min-height: 170px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #0d1b2a 0%, #1a2a40 50%, #101820 100%);
|
||||
}
|
||||
|
||||
.shuttle-card-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Overlay oscuro base (compacto) */
|
||||
@ -525,7 +536,7 @@ function getShiftLabel(shift: string) {
|
||||
rgba(0, 0, 0, 0.65) 55%,
|
||||
rgba(0, 0, 0, 0.30) 100%
|
||||
);
|
||||
z-index: 0;
|
||||
z-index: 1;
|
||||
transition: background 0.4s ease;
|
||||
}
|
||||
|
||||
@ -550,7 +561,7 @@ function getShiftLabel(shift: string) {
|
||||
.shuttle-main-info,
|
||||
.shuttle-details {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user