feat: replace map promo coupons with featured activities marker logic

This commit is contained in:
2026-03-18 18:54:39 -05:00
parent e2dff3fd5e
commit fd61cb7f87
5 changed files with 52 additions and 36 deletions

View File

@ -4,7 +4,7 @@
<!-- Header -->
<div class="sheet-header">
<div class="sheet-title-group">
<span class="sheet-title">{{ t('coupons.title') }}</span>
<span class="sheet-title">Actividades </span>
</div>
<button class="sheet-close" @click="$emit('close')">
<span class="material-icons">close</span>
@ -13,8 +13,8 @@
<!-- Card area with nav arrows wrapped in AuthGuard -->
<AuthGuard
:title="t('coupons.auth.title') || 'Ofertas Exclusivas'"
:message="t('coupons.auth.message') || 'Regístrate para ver todos los detalles de esta promoción y canjear tu código.'"
title="Explorar Actividad"
message="¿Quieres ver más detalles de esta actividad o contactar al proveedor?"
>
<div class="sheet-card-area">
<button class="sheet-nav" @click="$emit('prev')" :disabled="coupons.length < 2">
@ -22,22 +22,22 @@
</button>
<Transition name="carousel-slide" mode="out-in">
<div
<div
v-if="currentPromo"
:key="currentPromo.id"
class="sheet-card"
:style="{ backgroundImage: `url(${getImageUrl(currentPromo.image_url, 'coupon')})` }"
:style="{ backgroundImage: `url(${getImageUrl(currentPromo.image_url, 'business')})` }"
@mouseenter="$emit('pause')"
@touchstart="$emit('pause')"
@mouseleave="$emit('resume')"
>
<div class="sheet-card-overlay">
<div class="sheet-info">
<span class="sheet-biz-name">{{ currentPromo.business?.name || 'Local' }}</span>
<h3 class="sheet-promo-title">{{ currentPromo.title }}</h3>
<span class="sheet-biz-name">{{ currentPromo.category || 'Recomendado' }} {{ currentPromo.area || 'Chiriquí' }}</span>
<h3 class="sheet-promo-title">{{ currentPromo.name }}</h3>
<div class="sheet-actions">
<button class="sheet-cta" @click="$emit('promo-click', currentPromo)">{{ t('coupons.viewDetails') }}</button>
<span v-if="currentPromo.discount_percentage" class="sheet-discount-tag">-{{ currentPromo.discount_percentage }}%</span>
<button class="sheet-cta" @click="$emit('promo-click', currentPromo)">Ver Detalles</button>
<span v-if="currentPromo.supplier_name" class="sheet-discount-tag" style="background:#0F172A;">por {{ currentPromo.supplier_name }}</span>
</div>
</div>
</div>
@ -66,7 +66,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { getImageUrl } from '@/utils/imageUrl'
import AuthGuard from '@/components/common/AuthGuard.vue'
@ -78,8 +77,6 @@ const props = defineProps<{
defineEmits(['close', 'prev', 'next', 'pause', 'resume', 'promo-click', 'update:index'])
const { t } = useI18n()
const currentPromo = computed(() => {
if (props.coupons.length === 0) return null
const idx = props.currentIndex % props.coupons.length

View File

@ -2,7 +2,7 @@
import { supabase } from '@/supabase'
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 = {
/** Helper to upload file to supabase storage */
@ -44,7 +44,13 @@ export const businessService = {
// Comes as JSON string from the form
try { payload[key] = JSON.parse(value) } catch { /* ignore */ }
} 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
}
}
})

View File

@ -102,6 +102,7 @@ export interface Business {
website?: string | null
supplier_name?: string | null // Name of the business/operator
supplier_description?: string | null // Info about the business/operator
is_featured?: boolean // To show on Map
// Template/Mold fields
schedule?: string | null // "Lun-Sáb 8am-10pm"
whatsapp?: string | null // WhatsApp number

View File

@ -36,6 +36,7 @@ const businessForm = ref<Partial<Business>>({
facebook: '',
supplier_name: '',
supplier_description: '',
is_featured: false,
gallery_images: []
});
@ -122,6 +123,8 @@ async function saveBusiness() {
formData.append('facebook', businessForm.value.facebook || '');
formData.append('supplier_name', businessForm.value.supplier_name || '');
formData.append('supplier_description', businessForm.value.supplier_description || '');
formData.append('is_featured', String(businessForm.value.is_featured === true));
if (businessForm.value.gallery_images?.length) {
formData.append('gallery_images', JSON.stringify(businessForm.value.gallery_images));
@ -230,6 +233,16 @@ const catEmoji = computed(() => CATEGORY_EMOJI[businessForm.value.category || ''
</select>
</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">
<label>Descripción de la Actividad</label>

View File

@ -4,7 +4,7 @@ import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import { useRouteStore } from "@/stores/route";
import { useMapStore } from "@/stores/map";
import { useCouponStore } from "@/stores/coupon";
import { businessService } from "@/services/businessService";
import { useAuthStore } from "@/stores/auth";
import { useGoogleMaps } from "@/composables/useGoogleMaps";
import { analyticsService } from "@/services/analyticsService";
@ -29,8 +29,8 @@ const { t } = useI18n();
const routeStore = useRouteStore();
const mapStore = useMapStore();
const couponStore = useCouponStore();
const authStore = useAuthStore();
const featuredActivities = ref<any[]>([]);
const { map, isLoaded, error: mapsError, initMap, addHtmlMarker, setCenter, setZoom, addMarker, addCleanMarker } = useGoogleMaps();
const { estasCargando: estasCargandoRuta, errorRuta } = useDirectionsRoute();
@ -139,9 +139,8 @@ async function animateAndReload() {
}, 500);
}
function handlePromoClick(promo: any) {
selectedPromo.value = promo;
showPromoModal.value = true;
function handlePromoClick(act: any) {
router.push('/business/' + act.id);
}
function closePromoModal() {
@ -150,10 +149,12 @@ function closePromoModal() {
}
async function fetchData(isBackground = false) {
// Estas son actualizaciones asíncronas no urgentes para render principal
Promise.all([
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
@ -381,18 +382,16 @@ async function updatePromoMarkers() {
promoMarkers.value.forEach(m => m.setMap(null));
const newMarkers: any[] = [];
const promosWithCoords = couponStore.coupons.filter(c =>
c.is_active && c.business && c.business.latitude && c.business.longitude
);
const promosWithCoords = featuredActivities.value;
promosWithCoords.forEach(promo => {
const marker = addMarker(
{ lat: promo.business!.latitude!, lng: promo.business!.longitude! },
{ lat: promo.latitude!, lng: promo.longitude! },
{
title: promo.title,
title: promo.name,
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",
fillColor: '#FF4081',
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: '#FFD700',
fillOpacity: 1,
strokeColor: '#FFFFFF',
strokeWeight: 2,
@ -414,8 +413,8 @@ async function updatePromoMarkers() {
function startCarousel() {
if (carouselTimer.value) clearInterval(carouselTimer.value);
carouselTimer.value = setInterval(() => {
if (couponStore.coupons.length > 0) {
currentCarouselIndex.value = (currentCarouselIndex.value + 1) % couponStore.coupons.length;
if (featuredActivities.value.length > 0) {
currentCarouselIndex.value = (currentCarouselIndex.value + 1) % featuredActivities.value.length;
}
}, 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 class="map-floating-controls">
<button v-if="isLoaded && !showPromos && couponStore.coupons.length > 0" class="offers-fab pulse" @click="showPromos = true">
<span class="material-icons">local_offer</span>
<span v-if="couponStore.coupons.length > 0" class="offers-badge">{{ couponStore.coupons.length }}</span>
<button v-if="isLoaded && !showPromos && featuredActivities.length > 0" class="offers-fab pulse" @click="showPromos = true">
<span class="material-icons">star</span>
<span v-if="featuredActivities.length > 0" class="offers-badge">{{ featuredActivities.length }}</span>
</button>
<!-- 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 -->
<PromoCarousel
:is-open="showPromos"
:coupons="couponStore.coupons"
:coupons="featuredActivities"
:current-index="currentCarouselIndex"
@update:index="currentCarouselIndex = $event"
@close="showPromos = false"
@prev="currentCarouselIndex = (currentCarouselIndex - 1 + couponStore.coupons.length) % couponStore.coupons.length"
@next="currentCarouselIndex = (currentCarouselIndex + 1) % couponStore.coupons.length"
@prev="currentCarouselIndex = (currentCarouselIndex - 1 + featuredActivities.length) % featuredActivities.length"
@next="currentCarouselIndex = (currentCarouselIndex + 1) % featuredActivities.length"
@pause="stopCarousel"
@resume="startCarousel"
@promo-click="handlePromoClick"