Perf: Fase 2 optimización de imágenes y asincronía en interfaz finalizada

This commit is contained in:
2026-03-21 16:06:21 -05:00
parent 64c3bbb1d7
commit 3cda38bf8f
8 changed files with 43 additions and 61 deletions

View File

@ -1,4 +1,4 @@
<template> <template>
<div class="admin-drivers"> <div class="admin-drivers">
<div class="header"> <div class="header">
<button class="back-link" @click="router.push('/admin')"> Volver al Panel</button> <button class="back-link" @click="router.push('/admin')"> Volver al Panel</button>
@ -19,7 +19,7 @@
<div v-for="taxi in taxis" :key="taxi.id" class="item-card taxi-card"> <div v-for="taxi in taxis" :key="taxi.id" class="item-card taxi-card">
<div class="card-header"> <div class="card-header">
<div class="avatar"> <div class="avatar">
<img v-if="taxi.image_url" :src="getImageUrl(taxi.image_url)" alt="Taxi"> <AppImage v-if="taxi.image_url" :src="taxi.image_url" type="taxi" alt="Taxi" />
<span v-else class="material-icons">local_taxi</span> <span v-else class="material-icons">local_taxi</span>
</div> </div>
<div class="info"> <div class="info">
@ -167,9 +167,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, reactive, computed } from 'vue' import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { supabase } from '@/supabase' import { supabase } from '@/supabase'
import AppImage from '@/components/AppImage.vue'
const router = useRouter() const router = useRouter()
const isLoading = ref(false) const isLoading = ref(false)
@ -324,13 +325,6 @@ function getShiftLabel(shift: string) {
return labels[shift] || shift return labels[shift] || shift
} }
function getImageUrl(path: string) {
if (!path) return ''
if (path.startsWith('http')) return path
return path
}
</script> </script>
<style scoped> <style scoped>

View File

@ -5,7 +5,7 @@ import { businessService } from '@/services/businessService'
import { couponsService } from '@/services/couponsService' import { couponsService } from '@/services/couponsService'
import type { Business, Coupon } from '@/types' import type { Business, Coupon } from '@/types'
import FavoriteButton from '@/components/FavoriteButton.vue' import FavoriteButton from '@/components/FavoriteButton.vue'
import { getImageUrl as utilGetImageUrl } from '@/utils/imageUrl' import AppImage from '@/components/AppImage.vue'
import { analyticsService } from '@/services/analyticsService' import { analyticsService } from '@/services/analyticsService'
import LoadingBranded from '@/components/common/LoadingBranded.vue' import LoadingBranded from '@/components/common/LoadingBranded.vue'
@ -67,10 +67,6 @@ async function fetchData() {
onMounted(fetchData) onMounted(fetchData)
function getImageUrl(path: string | null | undefined) {
return utilGetImageUrl(path, 'business')
}
const goBack = () => router.back() const goBack = () => router.back()
function openMaps() { function openMaps() {
@ -189,10 +185,11 @@ const hasSocials = computed(() =>
<!-- SECTION 1 · HERO --> <!-- SECTION 1 · HERO -->
<!-- --> <!-- -->
<section class="hero"> <section class="hero">
<img <AppImage
:src="getImageUrl(business.image_url)" :src="business.image_url"
type="business"
:alt="business.name" :alt="business.name"
class="hero-img" imgClass="hero-img"
/> />
<!-- Gradient overlay --> <!-- Gradient overlay -->
<div class="hero-gradient"></div> <div class="hero-gradient"></div>
@ -266,11 +263,11 @@ const hasSocials = computed(() =>
class="carousel-slide" class="carousel-slide"
:class="{ 'slide-active': idx === carouselIndex, 'slide-prev': idx < carouselIndex, 'slide-next': idx > carouselIndex }" :class="{ 'slide-active': idx === carouselIndex, 'slide-prev': idx < carouselIndex, 'slide-next': idx > carouselIndex }"
> >
<img <AppImage
:src="getImageUrl(img)" :src="img"
type="business"
:alt="`Foto ${idx + 1} de ${business.name}`" :alt="`Foto ${idx + 1} de ${business.name}`"
class="carousel-img" imgClass="carousel-img"
loading="lazy"
/> />
</div> </div>
</div> </div>

View File

@ -5,7 +5,7 @@ import type { Business } from '@/types'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { analyticsService } from '@/services/analyticsService' import { analyticsService } from '@/services/analyticsService'
import { getImageUrl } from '@/utils/imageUrl' import AppImage from '@/components/AppImage.vue'
import AuthGuard from '@/components/common/AuthGuard.vue' import AuthGuard from '@/components/common/AuthGuard.vue'
import LoadingBranded from '@/components/common/LoadingBranded.vue' import LoadingBranded from '@/components/common/LoadingBranded.vue'
@ -215,8 +215,7 @@ function resetFilters() {
<TransitionGroup v-if="filteredBusinesses.length > 0" name="fade" tag="div" class="activity-grid"> <TransitionGroup v-if="filteredBusinesses.length > 0" name="fade" tag="div" class="activity-grid">
<div v-for="biz in filteredBusinesses" :key="biz.id" class="activity-card" @click="handleExplore(biz)"> <div v-for="biz in filteredBusinesses" :key="biz.id" class="activity-card" @click="handleExplore(biz)">
<div class="card-img-wrap"> <div class="card-img-wrap">
<img :src="getImageUrl(biz.image_url, 'business')" :alt="biz.name" loading="lazy" class="card-img" <AppImage :src="biz.image_url" type="business" :alt="biz.name" imgClass="card-img" />
@error="(e) => (e.target as HTMLImageElement).src = getImageUrl(null, 'business')" />
</div> </div>
<div class="card-info"> <div class="card-info">
<p class="card-title">{{ biz.name }}</p> <p class="card-title">{{ biz.name }}</p>
@ -244,8 +243,7 @@ function resetFilters() {
<div class="activity-grid"> <div class="activity-grid">
<div v-for="biz in featuredBusinesses" :key="biz.id" class="activity-card" @click="handleExplore(biz)"> <div v-for="biz in featuredBusinesses" :key="biz.id" class="activity-card" @click="handleExplore(biz)">
<div class="card-img-wrap"> <div class="card-img-wrap">
<img :src="getImageUrl(biz.image_url, 'business')" :alt="biz.name" loading="lazy" class="card-img" <AppImage :src="biz.image_url" type="business" :alt="biz.name" imgClass="card-img" />
@error="(e) => (e.target as HTMLImageElement).src = getImageUrl(null, 'business')" />
</div> </div>
<div class="card-info"> <div class="card-info">
<p class="card-title">{{ biz.name }}</p> <p class="card-title">{{ biz.name }}</p>
@ -260,8 +258,7 @@ function resetFilters() {
<TransitionGroup name="fade" tag="div" class="activity-grid"> <TransitionGroup name="fade" tag="div" class="activity-grid">
<div v-for="biz in gridBusinesses" :key="biz.id" class="activity-card" @click="handleExplore(biz)"> <div v-for="biz in gridBusinesses" :key="biz.id" class="activity-card" @click="handleExplore(biz)">
<div class="card-img-wrap"> <div class="card-img-wrap">
<img :src="getImageUrl(biz.image_url, 'business')" :alt="biz.name" loading="lazy" class="card-img" <AppImage :src="biz.image_url" type="business" :alt="biz.name" imgClass="card-img" />
@error="(e) => (e.target as HTMLImageElement).src = getImageUrl(null, 'business')" />
</div> </div>
<div class="card-info"> <div class="card-info">
<p class="card-title">{{ biz.name }}</p> <p class="card-title">{{ biz.name }}</p>

View File

@ -16,12 +16,14 @@ import { useETA } from "@/composables/useETA";
import { useFlujoPrincipal } from "@/composables/useFlujoPrincipal"; import { useFlujoPrincipal } from "@/composables/useFlujoPrincipal";
import { useMapState } from "@/composables/useMapState"; import { useMapState } from "@/composables/useMapState";
// Optimized Components (Extracted) // Optimized Components (Extracted) - Lazy Loaded
import SearchOverlay from "@/components/map/SearchOverlay.vue"; import { defineAsyncComponent } from 'vue';
import PromoCarousel from "@/components/map/PromoCarousel.vue";
import ArrivalBanner from "@/components/map/ArrivalBanner.vue"; const SearchOverlay = defineAsyncComponent(() => import("@/components/map/SearchOverlay.vue"));
const PromoCarousel = defineAsyncComponent(() => import("@/components/map/PromoCarousel.vue"));
const ArrivalBanner = defineAsyncComponent(() => import("@/components/map/ArrivalBanner.vue"));
const ETACard = defineAsyncComponent(() => import("@/components/map/ETACard.vue"));
import ETACard from "@/components/map/ETACard.vue";
import type { BusStop } from '@/types' import type { BusStop } from '@/types'
const router = useRouter(); const router = useRouter();

View File

@ -7,6 +7,7 @@ import { shuttlesService } from '@/services/shuttlesService'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { supabase } from '@/supabase' import { supabase } from '@/supabase'
import type { Coupon, Business, Shuttle } from '@/types' import type { Coupon, Business, Shuttle } from '@/types'
import AppImage from '@/components/AppImage.vue'
const route = useRoute() const route = useRoute()
const authStore = useAuthStore() const authStore = useAuthStore()
@ -203,11 +204,6 @@ function openEditModal(coupon: Coupon) {
showModal.value = true showModal.value = true
} }
function getImageUrl(path: string | null | undefined) {
if (!path) return '/default-coupon.png'
return path
}
async function saveCoupon() { async function saveCoupon() {
try { try {
if (!currentCoupon.value.title?.trim()) { if (!currentCoupon.value.title?.trim()) {
@ -378,7 +374,7 @@ async function toggleCouponStatus(coupon: Coupon) {
<td> <td>
<div class="title-cell"> <div class="title-cell">
<div class="coupon-header-cell"> <div class="coupon-header-cell">
<img :src="getImageUrl(coupon.image_url)" class="coupon-mini-img" /> <AppImage :src="coupon.image_url" type="coupon" imgClass="coupon-mini-img" />
<div> <div>
<strong>{{ coupon.title }}</strong> <strong>{{ coupon.title }}</strong>
<div class="business-tag"> <div class="business-tag">
@ -443,7 +439,7 @@ async function toggleCouponStatus(coupon: Coupon) {
<td> <td>
<div class="title-cell"> <div class="title-cell">
<div class="coupon-header-cell"> <div class="coupon-header-cell">
<img :src="getImageUrl(biz.image_url)" class="coupon-mini-img" /> <AppImage :src="biz.image_url" type="business" imgClass="coupon-mini-img" />
<strong>{{ biz.name }}</strong> <strong>{{ biz.name }}</strong>
</div> </div>
</div> </div>
@ -495,7 +491,7 @@ async function toggleCouponStatus(coupon: Coupon) {
<td> <td>
<div class="title-cell"> <div class="title-cell">
<div class="coupon-header-cell"> <div class="coupon-header-cell">
<img :src="getImageUrl(shuttle.image_url)" class="coupon-mini-img" /> <AppImage :src="shuttle.image_url" type="shuttle" imgClass="coupon-mini-img" />
<div> <div>
<strong>{{ shuttle.route_name }}</strong> <strong>{{ shuttle.route_name }}</strong>
<div class="business-tag"> <div class="business-tag">

View File

@ -3,7 +3,7 @@ import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { supabase } from '@/supabase' import { supabase } from '@/supabase'
import type { Shuttle } from '@/types' import type { Shuttle } from '@/types'
import { getImageUrl } from '@/utils/imageUrl' import AppImage from '@/components/AppImage.vue'
import { analyticsService } from '@/services/analyticsService' import { analyticsService } from '@/services/analyticsService'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import LoadingBranded from '@/components/common/LoadingBranded.vue' import LoadingBranded from '@/components/common/LoadingBranded.vue'
@ -101,11 +101,11 @@ const getTripTypeLabel = (type: string) => {
<div v-else-if="shuttle" class="px-4 py-4 space-y-4 max-w-lg mx-auto animate-fade-in"> <div v-else-if="shuttle" class="px-4 py-4 space-y-4 max-w-lg mx-auto animate-fade-in">
<!-- Imagen --> <!-- Imagen -->
<div v-if="shuttle.image_url" class="relative w-full h-56 md:h-64 rounded-2xl overflow-hidden shadow-sm"> <div v-if="shuttle.image_url" class="relative w-full h-56 md:h-64 rounded-2xl overflow-hidden shadow-sm">
<img <AppImage
:src="getImageUrl(shuttle.image_url, 'shuttle')" :src="shuttle.image_url"
type="shuttle"
:alt="shuttle.company_name" :alt="shuttle.company_name"
class="w-full h-full object-cover" imgClass="w-full h-full object-cover"
@error="(e) => (e.target as HTMLImageElement).src = getImageUrl(null, 'shuttle')"
/> />
<div class="absolute bottom-3 left-3 bg-[var(--bg-primary)]/90 backdrop-blur-sm px-3 py-1 rounded-full text-sm font-bold shadow-sm flex items-center gap-1"> <div class="absolute bottom-3 left-3 bg-[var(--bg-primary)]/90 backdrop-blur-sm px-3 py-1 rounded-full text-sm font-bold shadow-sm flex items-center gap-1">
<span class="material-icons text-sm" style="color: var(--active-color)">directions_bus</span> <span class="material-icons text-sm" style="color: var(--active-color)">directions_bus</span>

View File

@ -5,7 +5,7 @@ import { useTaxiStore } from '@/stores/taxi'
import { analyticsService } from '@/services/analyticsService' import { analyticsService } from '@/services/analyticsService'
import type { Taxi } from '@/types' import type { Taxi } from '@/types'
import FavoriteButton from '@/components/FavoriteButton.vue' import FavoriteButton from '@/components/FavoriteButton.vue'
import { getImageUrl } from '@/utils/imageUrl' import AppImage from '@/components/AppImage.vue'
import AuthGuard from '@/components/common/AuthGuard.vue' import AuthGuard from '@/components/common/AuthGuard.vue'
import LoadingBranded from '@/components/common/LoadingBranded.vue' import LoadingBranded from '@/components/common/LoadingBranded.vue'
@ -143,13 +143,11 @@ function getShiftLabel(shift: string) {
<div class="card-top"> <div class="card-top">
<div class="driver-avatar-wrap"> <div class="driver-avatar-wrap">
<div class="driver-avatar"> <div class="driver-avatar">
<img <AppImage
:src="getImageUrl(taxi.image_url, 'taxi')" :src="taxi.image_url"
loading="lazy" type="taxi"
decoding="async"
alt="Driver" alt="Driver"
@error="(e) => (e.target as HTMLImageElement).src = getImageUrl(null, 'taxi')" />
>
</div> </div>
<div class="driver-status" :class="{ 'status-online': isOnline(taxi) }"></div> <div class="driver-status" :class="{ 'status-online': isOnline(taxi) }"></div>
</div> </div>

View File

@ -4,7 +4,7 @@ import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useShuttleStore } from '@/stores/shuttle' import { useShuttleStore } from '@/stores/shuttle'
import AuthGuard from '@/components/common/AuthGuard.vue' import AuthGuard from '@/components/common/AuthGuard.vue'
import { getImageUrl } from '@/utils/imageUrl' import AppImage from '@/components/AppImage.vue'
import { analyticsService } from '@/services/analyticsService' import { analyticsService } from '@/services/analyticsService'
import LoadingBranded from '@/components/common/LoadingBranded.vue' import LoadingBranded from '@/components/common/LoadingBranded.vue'
@ -127,13 +127,11 @@ onUnmounted(() => {
@click="verDetalle(shuttle.id, shuttle.company_name || `${shuttle.origin}-${shuttle.destination}`)" @click="verDetalle(shuttle.id, shuttle.company_name || `${shuttle.origin}-${shuttle.destination}`)"
> >
<div class="card-image-wrap"> <div class="card-image-wrap">
<img <AppImage
:src="getImageUrl(shuttle.image_url, 'shuttle')" :src="shuttle.image_url"
loading="lazy" type="shuttle"
decoding="async" imgClass="shuttle-img"
class="shuttle-img"
alt="Shuttle" alt="Shuttle"
@error="(e) => (e.target as HTMLImageElement).src = getImageUrl(null, 'shuttle')"
/> />
<div class="company-tag" v-if="shuttle.company_name"> <div class="company-tag" v-if="shuttle.company_name">
<span class="material-icons">business</span> <span class="material-icons">business</span>