feat: replace map promo coupons with featured activities marker logic
This commit is contained in:
@ -4,7 +4,7 @@
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="sheet-header">
|
<div class="sheet-header">
|
||||||
<div class="sheet-title-group">
|
<div class="sheet-title-group">
|
||||||
<span class="sheet-title">{{ t('coupons.title') }}</span>
|
<span class="sheet-title">Actividades ⭐</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="sheet-close" @click="$emit('close')">
|
<button class="sheet-close" @click="$emit('close')">
|
||||||
<span class="material-icons">close</span>
|
<span class="material-icons">close</span>
|
||||||
@ -13,8 +13,8 @@
|
|||||||
|
|
||||||
<!-- Card area with nav arrows wrapped in AuthGuard -->
|
<!-- Card area with nav arrows wrapped in AuthGuard -->
|
||||||
<AuthGuard
|
<AuthGuard
|
||||||
:title="t('coupons.auth.title') || 'Ofertas Exclusivas'"
|
title="Explorar Actividad"
|
||||||
:message="t('coupons.auth.message') || 'Regístrate para ver todos los detalles de esta promoción y canjear tu código.'"
|
message="¿Quieres ver más detalles de esta actividad o contactar al proveedor?"
|
||||||
>
|
>
|
||||||
<div class="sheet-card-area">
|
<div class="sheet-card-area">
|
||||||
<button class="sheet-nav" @click="$emit('prev')" :disabled="coupons.length < 2">
|
<button class="sheet-nav" @click="$emit('prev')" :disabled="coupons.length < 2">
|
||||||
@ -22,22 +22,22 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Transition name="carousel-slide" mode="out-in">
|
<Transition name="carousel-slide" mode="out-in">
|
||||||
<div
|
<div
|
||||||
v-if="currentPromo"
|
v-if="currentPromo"
|
||||||
:key="currentPromo.id"
|
:key="currentPromo.id"
|
||||||
class="sheet-card"
|
class="sheet-card"
|
||||||
:style="{ backgroundImage: `url(${getImageUrl(currentPromo.image_url, 'coupon')})` }"
|
:style="{ backgroundImage: `url(${getImageUrl(currentPromo.image_url, 'business')})` }"
|
||||||
@mouseenter="$emit('pause')"
|
@mouseenter="$emit('pause')"
|
||||||
@touchstart="$emit('pause')"
|
@touchstart="$emit('pause')"
|
||||||
@mouseleave="$emit('resume')"
|
@mouseleave="$emit('resume')"
|
||||||
>
|
>
|
||||||
<div class="sheet-card-overlay">
|
<div class="sheet-card-overlay">
|
||||||
<div class="sheet-info">
|
<div class="sheet-info">
|
||||||
<span class="sheet-biz-name">{{ currentPromo.business?.name || 'Local' }}</span>
|
<span class="sheet-biz-name">{{ currentPromo.category || 'Recomendado' }} • {{ currentPromo.area || 'Chiriquí' }}</span>
|
||||||
<h3 class="sheet-promo-title">{{ currentPromo.title }}</h3>
|
<h3 class="sheet-promo-title">{{ currentPromo.name }}</h3>
|
||||||
<div class="sheet-actions">
|
<div class="sheet-actions">
|
||||||
<button class="sheet-cta" @click="$emit('promo-click', currentPromo)">{{ t('coupons.viewDetails') }}</button>
|
<button class="sheet-cta" @click="$emit('promo-click', currentPromo)">Ver Detalles</button>
|
||||||
<span v-if="currentPromo.discount_percentage" class="sheet-discount-tag">-{{ currentPromo.discount_percentage }}%</span>
|
<span v-if="currentPromo.supplier_name" class="sheet-discount-tag" style="background:#0F172A;">por {{ currentPromo.supplier_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -66,7 +66,6 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { getImageUrl } from '@/utils/imageUrl'
|
import { getImageUrl } from '@/utils/imageUrl'
|
||||||
import AuthGuard from '@/components/common/AuthGuard.vue'
|
import AuthGuard from '@/components/common/AuthGuard.vue'
|
||||||
|
|
||||||
@ -78,8 +77,6 @@ const props = defineProps<{
|
|||||||
|
|
||||||
defineEmits(['close', 'prev', 'next', 'pause', 'resume', 'promo-click', 'update:index'])
|
defineEmits(['close', 'prev', 'next', 'pause', 'resume', 'promo-click', 'update:index'])
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
const currentPromo = computed(() => {
|
const currentPromo = computed(() => {
|
||||||
if (props.coupons.length === 0) return null
|
if (props.coupons.length === 0) return null
|
||||||
const idx = props.currentIndex % props.coupons.length
|
const idx = props.currentIndex % props.coupons.length
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import { supabase } from '@/supabase'
|
import { supabase } from '@/supabase'
|
||||||
import type { Business } from '@/types'
|
import type { Business } from '@/types'
|
||||||
|
|
||||||
const SELECT_FIELDS = 'id, name, address, phone, image_url, social_media, category, latitude, longitude, area, description, website, supplier_name, supplier_description, schedule, whatsapp, instagram, facebook, gallery_images, updated_at'
|
const SELECT_FIELDS = 'id, name, address, phone, image_url, social_media, category, latitude, longitude, area, description, website, supplier_name, supplier_description, schedule, whatsapp, instagram, facebook, gallery_images, updated_at, is_featured'
|
||||||
|
|
||||||
export const businessService = {
|
export const businessService = {
|
||||||
/** Helper to upload file to supabase storage */
|
/** Helper to upload file to supabase storage */
|
||||||
@ -44,7 +44,13 @@ export const businessService = {
|
|||||||
// Comes as JSON string from the form
|
// Comes as JSON string from the form
|
||||||
try { payload[key] = JSON.parse(value) } catch { /* ignore */ }
|
try { payload[key] = JSON.parse(value) } catch { /* ignore */ }
|
||||||
} else if (value !== 'null' && value !== '' && key !== 'file' && key !== 'image') {
|
} else if (value !== 'null' && value !== '' && key !== 'file' && key !== 'image') {
|
||||||
payload[key] = value
|
if (value === 'true') {
|
||||||
|
payload[key] = true
|
||||||
|
} else if (value === 'false') {
|
||||||
|
payload[key] = false
|
||||||
|
} else {
|
||||||
|
payload[key] = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -102,6 +102,7 @@ export interface Business {
|
|||||||
website?: string | null
|
website?: string | null
|
||||||
supplier_name?: string | null // Name of the business/operator
|
supplier_name?: string | null // Name of the business/operator
|
||||||
supplier_description?: string | null // Info about the business/operator
|
supplier_description?: string | null // Info about the business/operator
|
||||||
|
is_featured?: boolean // To show on Map
|
||||||
// Template/Mold fields
|
// Template/Mold fields
|
||||||
schedule?: string | null // "Lun-Sáb 8am-10pm"
|
schedule?: string | null // "Lun-Sáb 8am-10pm"
|
||||||
whatsapp?: string | null // WhatsApp number
|
whatsapp?: string | null // WhatsApp number
|
||||||
|
|||||||
@ -36,6 +36,7 @@ const businessForm = ref<Partial<Business>>({
|
|||||||
facebook: '',
|
facebook: '',
|
||||||
supplier_name: '',
|
supplier_name: '',
|
||||||
supplier_description: '',
|
supplier_description: '',
|
||||||
|
is_featured: false,
|
||||||
gallery_images: []
|
gallery_images: []
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -122,6 +123,8 @@ async function saveBusiness() {
|
|||||||
formData.append('facebook', businessForm.value.facebook || '');
|
formData.append('facebook', businessForm.value.facebook || '');
|
||||||
formData.append('supplier_name', businessForm.value.supplier_name || '');
|
formData.append('supplier_name', businessForm.value.supplier_name || '');
|
||||||
formData.append('supplier_description', businessForm.value.supplier_description || '');
|
formData.append('supplier_description', businessForm.value.supplier_description || '');
|
||||||
|
formData.append('is_featured', String(businessForm.value.is_featured === true));
|
||||||
|
|
||||||
|
|
||||||
if (businessForm.value.gallery_images?.length) {
|
if (businessForm.value.gallery_images?.length) {
|
||||||
formData.append('gallery_images', JSON.stringify(businessForm.value.gallery_images));
|
formData.append('gallery_images', JSON.stringify(businessForm.value.gallery_images));
|
||||||
@ -230,6 +233,16 @@ const catEmoji = computed(() => CATEGORY_EMOJI[businessForm.value.category || ''
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group checkbox-box" style="margin-top:-8px; margin-bottom:16px;">
|
||||||
|
<label style="display:flex; align-items:center; cursor:pointer;" class="text-white">
|
||||||
|
<input type="checkbox" v-model="businessForm.is_featured" style="margin-right: 8px; width: 18px; height: 18px;">
|
||||||
|
⭐ Destacar en el Mapa principal
|
||||||
|
</label>
|
||||||
|
<p style="font-size: 0.75rem; color: #94a3b8; margin-top: 4px; margin-left: 26px;">
|
||||||
|
Aparecerá en el recuadro flotante del mapa principal como una actividad recomendada.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Descripción de la Actividad</label>
|
<label>Descripción de la Actividad</label>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { useRouter } from "vue-router";
|
|||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useRouteStore } from "@/stores/route";
|
import { useRouteStore } from "@/stores/route";
|
||||||
import { useMapStore } from "@/stores/map";
|
import { useMapStore } from "@/stores/map";
|
||||||
import { useCouponStore } from "@/stores/coupon";
|
import { businessService } from "@/services/businessService";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useGoogleMaps } from "@/composables/useGoogleMaps";
|
import { useGoogleMaps } from "@/composables/useGoogleMaps";
|
||||||
import { analyticsService } from "@/services/analyticsService";
|
import { analyticsService } from "@/services/analyticsService";
|
||||||
@ -29,8 +29,8 @@ const { t } = useI18n();
|
|||||||
|
|
||||||
const routeStore = useRouteStore();
|
const routeStore = useRouteStore();
|
||||||
const mapStore = useMapStore();
|
const mapStore = useMapStore();
|
||||||
const couponStore = useCouponStore();
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
const featuredActivities = ref<any[]>([]);
|
||||||
|
|
||||||
const { map, isLoaded, error: mapsError, initMap, addHtmlMarker, setCenter, setZoom, addMarker, addCleanMarker } = useGoogleMaps();
|
const { map, isLoaded, error: mapsError, initMap, addHtmlMarker, setCenter, setZoom, addMarker, addCleanMarker } = useGoogleMaps();
|
||||||
const { estasCargando: estasCargandoRuta, errorRuta } = useDirectionsRoute();
|
const { estasCargando: estasCargandoRuta, errorRuta } = useDirectionsRoute();
|
||||||
@ -139,9 +139,8 @@ async function animateAndReload() {
|
|||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePromoClick(promo: any) {
|
function handlePromoClick(act: any) {
|
||||||
selectedPromo.value = promo;
|
router.push('/business/' + act.id);
|
||||||
showPromoModal.value = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closePromoModal() {
|
function closePromoModal() {
|
||||||
@ -150,10 +149,12 @@ function closePromoModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchData(isBackground = false) {
|
async function fetchData(isBackground = false) {
|
||||||
// Estas son actualizaciones asíncronas no urgentes para render principal
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
routeStore.loadRoutes(undefined, false, isBackground),
|
routeStore.loadRoutes(undefined, false, isBackground),
|
||||||
couponStore.loadCoupons({ active_only: true }, isBackground)
|
businessService.getAllBusinesses().then(b => {
|
||||||
|
featuredActivities.value = b.filter(biz => biz.is_featured && biz.latitude && biz.longitude);
|
||||||
|
if (showPromos.value) updatePromoMarkers();
|
||||||
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Update super-urgente, dibuja los buses actuales en el mapa al instante
|
// Update super-urgente, dibuja los buses actuales en el mapa al instante
|
||||||
@ -381,18 +382,16 @@ async function updatePromoMarkers() {
|
|||||||
promoMarkers.value.forEach(m => m.setMap(null));
|
promoMarkers.value.forEach(m => m.setMap(null));
|
||||||
const newMarkers: any[] = [];
|
const newMarkers: any[] = [];
|
||||||
|
|
||||||
const promosWithCoords = couponStore.coupons.filter(c =>
|
const promosWithCoords = featuredActivities.value;
|
||||||
c.is_active && c.business && c.business.latitude && c.business.longitude
|
|
||||||
);
|
|
||||||
|
|
||||||
promosWithCoords.forEach(promo => {
|
promosWithCoords.forEach(promo => {
|
||||||
const marker = addMarker(
|
const marker = addMarker(
|
||||||
{ lat: promo.business!.latitude!, lng: promo.business!.longitude! },
|
{ lat: promo.latitude!, lng: promo.longitude! },
|
||||||
{
|
{
|
||||||
title: promo.title,
|
title: promo.name,
|
||||||
icon: {
|
icon: {
|
||||||
path: "M20 6h-2.18c.11-.31.18-.65.18-1 0-1.66-1.34-3-3-3-1.05 0-1.96.54-2.5 1.35l-.5.65-.5-.65C10.96 2.54 10.05 2 9 2 7.34 2 6 3.34 6 5c0 .35.07.69.18 1H4c-1.11 0-1.99.89-1.99 2L2 19c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-5-2c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zM9 4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm11 15H4v-2h16v2zm0-5H4V8h5.08L7 10.83 8.62 12 11 8.76l1-1.36 1 1.36L15.38 12 17 10.83 14.92 8H20v6z",
|
path: "M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z",
|
||||||
fillColor: '#FF4081',
|
fillColor: '#FFD700',
|
||||||
fillOpacity: 1,
|
fillOpacity: 1,
|
||||||
strokeColor: '#FFFFFF',
|
strokeColor: '#FFFFFF',
|
||||||
strokeWeight: 2,
|
strokeWeight: 2,
|
||||||
@ -414,8 +413,8 @@ async function updatePromoMarkers() {
|
|||||||
function startCarousel() {
|
function startCarousel() {
|
||||||
if (carouselTimer.value) clearInterval(carouselTimer.value);
|
if (carouselTimer.value) clearInterval(carouselTimer.value);
|
||||||
carouselTimer.value = setInterval(() => {
|
carouselTimer.value = setInterval(() => {
|
||||||
if (couponStore.coupons.length > 0) {
|
if (featuredActivities.value.length > 0) {
|
||||||
currentCarouselIndex.value = (currentCarouselIndex.value + 1) % couponStore.coupons.length;
|
currentCarouselIndex.value = (currentCarouselIndex.value + 1) % featuredActivities.value.length;
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
@ -610,9 +609,9 @@ watch([() => authStore.userProfile?.auto_location, isLoaded], ([canLocate, loade
|
|||||||
<div id="map" class="map" :style="{ display: isLoaded ? 'block' : 'none' }"></div>
|
<div id="map" class="map" :style="{ display: isLoaded ? 'block' : 'none' }"></div>
|
||||||
|
|
||||||
<div class="map-floating-controls">
|
<div class="map-floating-controls">
|
||||||
<button v-if="isLoaded && !showPromos && couponStore.coupons.length > 0" class="offers-fab pulse" @click="showPromos = true">
|
<button v-if="isLoaded && !showPromos && featuredActivities.length > 0" class="offers-fab pulse" @click="showPromos = true">
|
||||||
<span class="material-icons">local_offer</span>
|
<span class="material-icons">star</span>
|
||||||
<span v-if="couponStore.coupons.length > 0" class="offers-badge">{{ couponStore.coupons.length }}</span>
|
<span v-if="featuredActivities.length > 0" class="offers-badge">{{ featuredActivities.length }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- SMART LOCATION BUTTON: Hidden by default if auto-location is active, shows up with text when map moved -->
|
<!-- SMART LOCATION BUTTON: Hidden by default if auto-location is active, shows up with text when map moved -->
|
||||||
@ -662,12 +661,12 @@ watch([() => authStore.userProfile?.auto_location, isLoaded], ([canLocate, loade
|
|||||||
<!-- COMPONENTIZED PROMOS -->
|
<!-- COMPONENTIZED PROMOS -->
|
||||||
<PromoCarousel
|
<PromoCarousel
|
||||||
:is-open="showPromos"
|
:is-open="showPromos"
|
||||||
:coupons="couponStore.coupons"
|
:coupons="featuredActivities"
|
||||||
:current-index="currentCarouselIndex"
|
:current-index="currentCarouselIndex"
|
||||||
@update:index="currentCarouselIndex = $event"
|
@update:index="currentCarouselIndex = $event"
|
||||||
@close="showPromos = false"
|
@close="showPromos = false"
|
||||||
@prev="currentCarouselIndex = (currentCarouselIndex - 1 + couponStore.coupons.length) % couponStore.coupons.length"
|
@prev="currentCarouselIndex = (currentCarouselIndex - 1 + featuredActivities.length) % featuredActivities.length"
|
||||||
@next="currentCarouselIndex = (currentCarouselIndex + 1) % couponStore.coupons.length"
|
@next="currentCarouselIndex = (currentCarouselIndex + 1) % featuredActivities.length"
|
||||||
@pause="stopCarousel"
|
@pause="stopCarousel"
|
||||||
@resume="startCarousel"
|
@resume="startCarousel"
|
||||||
@promo-click="handlePromoClick"
|
@promo-click="handlePromoClick"
|
||||||
|
|||||||
Reference in New Issue
Block a user