Perf: Fase 2 optimización de imágenes y asincronía en interfaz finalizada
This commit is contained in:
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user