feat: replace map promo coupons with featured activities marker logic
This commit is contained in:
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
|
||||
Reference in New Issue
Block a user