feat: refactor discover page and detail view for activities
This commit is contained in:
1717
frontend/package-lock.json
generated
1717
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -16,6 +16,7 @@ import { useCouponStore } from './stores/coupon'
|
|||||||
import { supabase } from '@/supabase'
|
import { supabase } from '@/supabase'
|
||||||
import { useGoogleMaps } from '@/composables/useGoogleMaps'
|
import { useGoogleMaps } from '@/composables/useGoogleMaps'
|
||||||
// useFavoritesStore ya importado arriba (línea 8)
|
// useFavoritesStore ya importado arriba (línea 8)
|
||||||
|
import { useRegisterSW } from 'virtual:pwa-register/vue'
|
||||||
|
|
||||||
// Initialize theme store
|
// Initialize theme store
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@ -28,6 +29,11 @@ const { loadMaps } = useGoogleMaps()
|
|||||||
// Iniciar descarga asíncrona masiva de Google Maps al bootear VUE (Ahorra ~3 seg de espera en MapView)
|
// Iniciar descarga asíncrona masiva de Google Maps al bootear VUE (Ahorra ~3 seg de espera en MapView)
|
||||||
loadMaps()
|
loadMaps()
|
||||||
|
|
||||||
|
const { needRefresh, updateServiceWorker } = useRegisterSW()
|
||||||
|
const reloadPWA = () => {
|
||||||
|
updateServiceWorker(true)
|
||||||
|
}
|
||||||
|
|
||||||
const isLandingPage = computed(() => route.name === 'landing')
|
const isLandingPage = computed(() => route.name === 'landing')
|
||||||
const isSplashScreen = computed(() => route.name === 'splash')
|
const isSplashScreen = computed(() => route.name === 'splash')
|
||||||
const isAuthScreen = computed(() => {
|
const isAuthScreen = computed(() => {
|
||||||
@ -184,6 +190,11 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div v-if="needRefresh" class="pwa-update-toast">
|
||||||
|
<p>¡Nueva versión disponible!</p>
|
||||||
|
<button @click="reloadPWA">Actualizar ahora</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<MainLayout v-if="!isSplashScreen && !isAuthScreen && !isLandingPage">
|
<MainLayout v-if="!isSplashScreen && !isAuthScreen && !isLandingPage">
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
@ -269,6 +280,49 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* PWA Update Toast */
|
||||||
|
.pwa-update-toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 2rem;
|
||||||
|
right: 2rem;
|
||||||
|
background: var(--active-color);
|
||||||
|
color: #000;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
box-shadow: 0 10px 25px rgba(254, 231, 21, 0.4);
|
||||||
|
font-weight: 600;
|
||||||
|
animation: slideInUp 0.3s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-update-toast p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-update-toast button {
|
||||||
|
background: #000;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-update-toast button:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInUp {
|
||||||
|
from { opacity: 0; transform: translateY(20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
/* Global Utilities */
|
/* Global Utilities */
|
||||||
.glass-effect {
|
.glass-effect {
|
||||||
background: var(--glass-bg);
|
background: var(--glass-bg);
|
||||||
|
|||||||
@ -216,41 +216,41 @@
|
|||||||
"accessible": "Accesible"
|
"accessible": "Accesible"
|
||||||
},
|
},
|
||||||
"discover": {
|
"discover": {
|
||||||
"title": "¡Explora Chiriquí! 🌿",
|
"title": "Experiencias y Actividades 🌟",
|
||||||
"subtitle": "Descubre los mejores lugares cerca de ti",
|
"subtitle": "Descubre increíbles actividades en Chiriquí",
|
||||||
"searchPlaceholder": "Buscar restaurantes, hoteles...",
|
"searchPlaceholder": "Buscar tours, paseos, clases...",
|
||||||
"filterLabel": "Filtrar por área:",
|
"filterLabel": "Filtrar por área:",
|
||||||
"allAreas": "Todas",
|
"allAreas": "Todas",
|
||||||
"loading": "Cargando lugares...",
|
"loading": "Cargando actividades...",
|
||||||
"error": "No se pudieron cargar los lugares. Revisa tu conexión.",
|
"error": "No se pudieron cargar las actividades. Revisa tu conexión.",
|
||||||
"empty": "No hay lugares aún",
|
"empty": "No hay actividades aún",
|
||||||
"emptyDesc": "Pronto habrá negocios y lugares turísticos disponibles aquí.",
|
"emptyDesc": "Pronto habrá emocionantes actividades disponibles aquí.",
|
||||||
"noResults": "Sin resultados",
|
"noResults": "Sin resultados",
|
||||||
"noResultsDesc": "No encontramos lugares con ese filtro.",
|
"noResultsDesc": "No encontramos actividades con ese filtro.",
|
||||||
"viewAll": "Ver todos los lugares",
|
"viewAll": "Ver todas",
|
||||||
"exploreMore": "Explorar Lugar",
|
"exploreMore": "Ver Actividad",
|
||||||
"tourism": "Turismo",
|
"tourism": "Turismo",
|
||||||
"results": "{count} lugar | {count} lugares",
|
"results": "{count} actividad | {count} actividades",
|
||||||
"in": "en",
|
"in": "en",
|
||||||
"lookingMore": "¿Buscas algo más?",
|
"lookingMore": "¿Buscas algo más?",
|
||||||
"exploreWithoutFilters": "Explora sin filtros para descubrir todo",
|
"exploreWithoutFilters": "Explora sin filtros para descubrir todo",
|
||||||
"sections": {
|
"sections": {
|
||||||
"byArea": "🗺️ Por Área",
|
"byArea": "🗺️ Por Área",
|
||||||
"featured": "✨ Destacados",
|
"featured": "✨ Populares",
|
||||||
"allPlaces": "🏙️ Todos los lugares"
|
"allPlaces": "🎒 Todas las actividades"
|
||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"all": "Todas",
|
"all": "Todas",
|
||||||
"restaurant": "Restaurante",
|
"restaurant": "Gastronomía",
|
||||||
"hotel": "Hotel",
|
"hotel": "Hospedaje",
|
||||||
"cafe": "Café",
|
"cafe": "Cafeterías",
|
||||||
"commerce": "Comercio",
|
"commerce": "Compras",
|
||||||
"tourism": "Turismo",
|
"tourism": "Tours",
|
||||||
"drinks": "Bebidas"
|
"drinks": "Vida Nocturna"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"title": "¡Locales exclusivos!",
|
"title": "¡Actividades únicas!",
|
||||||
"message": "Para descubrir Chiriquí regístrate o accede. Encuentra los mejores rincones y ofertas directas."
|
"message": "Para descubrir lo mejor de Chiriquí regístrate o accede. Encuentra tours y experiencias exclusivas."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"business": {
|
"business": {
|
||||||
@ -277,8 +277,8 @@
|
|||||||
"offlineDesc": "No pudimos cargar los datos. Revisa tu internet e intenta de nuevo.",
|
"offlineDesc": "No pudimos cargar los datos. Revisa tu internet e intenta de nuevo.",
|
||||||
"website": "Sitio Web",
|
"website": "Sitio Web",
|
||||||
"gallery": "📸 Galería",
|
"gallery": "📸 Galería",
|
||||||
"about": "Sobre el lugar",
|
"about": "Acerca de la actividad",
|
||||||
"contactUs": "Contáctanos",
|
"contactUs": "Proveedor / Operador",
|
||||||
"offers": "🎁 Ofertas Disponibles",
|
"offers": "🎁 Ofertas Disponibles",
|
||||||
"viewMap": "Ver en el Mapa",
|
"viewMap": "Ver en el Mapa",
|
||||||
"call": "Llamar",
|
"call": "Llamar",
|
||||||
|
|||||||
@ -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, 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'
|
||||||
|
|
||||||
export const businessService = {
|
export const businessService = {
|
||||||
/** Helper to upload file to supabase storage */
|
/** Helper to upload file to supabase storage */
|
||||||
|
|||||||
@ -100,6 +100,8 @@ export interface Business {
|
|||||||
area?: string | null
|
area?: string | null
|
||||||
description?: string | null
|
description?: string | null
|
||||||
website?: string | null
|
website?: string | null
|
||||||
|
supplier_name?: string | null // Name of the business/operator
|
||||||
|
supplier_description?: string | null // Info about the business/operator
|
||||||
// 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
|
||||||
|
|||||||
@ -34,6 +34,8 @@ const businessForm = ref<Partial<Business>>({
|
|||||||
whatsapp: '',
|
whatsapp: '',
|
||||||
instagram: '',
|
instagram: '',
|
||||||
facebook: '',
|
facebook: '',
|
||||||
|
supplier_name: '',
|
||||||
|
supplier_description: '',
|
||||||
gallery_images: []
|
gallery_images: []
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -118,6 +120,8 @@ async function saveBusiness() {
|
|||||||
formData.append('whatsapp', businessForm.value.whatsapp || '');
|
formData.append('whatsapp', businessForm.value.whatsapp || '');
|
||||||
formData.append('instagram', businessForm.value.instagram || '');
|
formData.append('instagram', businessForm.value.instagram || '');
|
||||||
formData.append('facebook', businessForm.value.facebook || '');
|
formData.append('facebook', businessForm.value.facebook || '');
|
||||||
|
formData.append('supplier_name', businessForm.value.supplier_name || '');
|
||||||
|
formData.append('supplier_description', businessForm.value.supplier_description || '');
|
||||||
|
|
||||||
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));
|
||||||
@ -170,7 +174,7 @@ const catEmoji = computed(() => CATEGORY_EMOJI[businessForm.value.category || ''
|
|||||||
<button class="back-btn" @click="router.back()">
|
<button class="back-btn" @click="router.back()">
|
||||||
<span class="material-icons">arrow_back</span>
|
<span class="material-icons">arrow_back</span>
|
||||||
</button>
|
</button>
|
||||||
<h1>{{ isEditing ? 'Editar Negocio' : 'Nuevo Negocio' }}</h1>
|
<h1>{{ isEditing ? 'Editar Actividad' : 'Nueva Actividad' }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin-grid-layout">
|
<div class="admin-grid-layout">
|
||||||
@ -178,14 +182,14 @@ const catEmoji = computed(() => CATEGORY_EMOJI[businessForm.value.category || ''
|
|||||||
<section class="form-panel nexus-glass">
|
<section class="form-panel nexus-glass">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<span class="material-icons">storefront</span>
|
<span class="material-icons">storefront</span>
|
||||||
<h2>Datos del Negocio</h2>
|
<h2>Datos de la Actividad</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nexus-form">
|
<div class="nexus-form">
|
||||||
<div class="form-group grid-row">
|
<div class="form-group grid-row">
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<label>Nombre del Negocio *</label>
|
<label>Nombre de la Actividad *</label>
|
||||||
<input v-model="businessForm.name" type="text" placeholder="Ej: Restaurante La Casona" required>
|
<input v-model="businessForm.name" type="text" placeholder="Ej: Tour Gastronómico / Restaurante La Casona" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<label>Imagen de Portada</label>
|
<label>Imagen de Portada</label>
|
||||||
@ -225,7 +229,7 @@ const catEmoji = computed(() => CATEGORY_EMOJI[businessForm.value.category || ''
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Descripción del Lugar</label>
|
<label>Descripción de la Actividad</label>
|
||||||
<textarea
|
<textarea
|
||||||
v-model="businessForm.description"
|
v-model="businessForm.description"
|
||||||
placeholder="Describe el ambiente, la especialidad, qué lo hace único..."
|
placeholder="Describe el ambiente, la especialidad, qué lo hace único..."
|
||||||
@ -250,8 +254,28 @@ const catEmoji = computed(() => CATEGORY_EMOJI[businessForm.value.category || ''
|
|||||||
<input v-model="businessForm.address" type="text" placeholder="Ej: Calle Principal #123">
|
<input v-model="businessForm.address" type="text" placeholder="Ej: Calle Principal #123">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-section-label mt-4">Redes y Contacto Directo</div>
|
<div class="form-section-label mt-4">Detalles del Proveedor / Operador</div>
|
||||||
|
|
||||||
|
<div class="form-group grid-row" style="margin-top: 12px;">
|
||||||
|
<div class="input-box">
|
||||||
|
<label>Nombre del Operador / Empresa</label>
|
||||||
|
<input v-model="businessForm.supplier_name" type="text" placeholder="Ej: Panama Tours S.A.">
|
||||||
|
</div>
|
||||||
|
<div class="input-box">
|
||||||
|
<label>Página Web</label>
|
||||||
|
<input v-model="businessForm.website" type="text" placeholder="https://">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-bottom: 12px;">
|
||||||
|
<label>Quiénes Somos (Información del Proveedor)</label>
|
||||||
|
<textarea
|
||||||
|
v-model="businessForm.supplier_description"
|
||||||
|
placeholder="Habla sobre la experiencia de la empresa que ofrece el servicio..."
|
||||||
|
rows="2"
|
||||||
|
class="nexus-textarea"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
<div class="form-group grid-row">
|
<div class="form-group grid-row">
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<label>WhatsApp (Sin +)</label>
|
<label>WhatsApp (Sin +)</label>
|
||||||
@ -266,11 +290,6 @@ const catEmoji = computed(() => CATEGORY_EMOJI[businessForm.value.category || ''
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Página Web</label>
|
|
||||||
<input v-model="businessForm.website" type="text" placeholder="https://">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- GALERIA DE IMAGENES -->
|
<!-- GALERIA DE IMAGENES -->
|
||||||
<div class="form-section-label mt-4">📸 Galería de Imágenes</div>
|
<div class="form-section-label mt-4">📸 Galería de Imágenes</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@ -309,7 +328,7 @@ const catEmoji = computed(() => CATEGORY_EMOJI[businessForm.value.category || ''
|
|||||||
|
|
||||||
<button class="deploy-btn" :disabled="isLoading" @click="saveBusiness">
|
<button class="deploy-btn" :disabled="isLoading" @click="saveBusiness">
|
||||||
<span class="material-icons">{{ isLoading ? 'sync' : 'save' }}</span>
|
<span class="material-icons">{{ isLoading ? 'sync' : 'save' }}</span>
|
||||||
{{ isLoading ? 'GUARDANDO...' : 'GUARDAR NEGOCIO' }}
|
{{ isLoading ? 'GUARDANDO...' : 'GUARDAR ACTIVIDAD' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p v-if="showMessage.text" :class="['message', showMessage.type]">{{ showMessage.text }}</p>
|
<p v-if="showMessage.text" :class="['message', showMessage.type]">{{ showMessage.text }}</p>
|
||||||
@ -334,7 +353,7 @@ const catEmoji = computed(() => CATEGORY_EMOJI[businessForm.value.category || ''
|
|||||||
|
|
||||||
<div class="hero-body">
|
<div class="hero-body">
|
||||||
<span class="hero-cat">{{ catEmoji }} {{ businessForm.category }}</span>
|
<span class="hero-cat">{{ catEmoji }} {{ businessForm.category }}</span>
|
||||||
<h1 class="hero-name">{{ businessForm.name || 'Nombre del Negocio' }}</h1>
|
<h1 class="hero-name">{{ businessForm.name || 'Nombre de la Actividad' }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-rule"></div>
|
<div class="hero-rule"></div>
|
||||||
</section>
|
</section>
|
||||||
@ -348,22 +367,12 @@ const catEmoji = computed(() => CATEGORY_EMOJI[businessForm.value.category || ''
|
|||||||
<section class="biz-section">
|
<section class="biz-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<span class="section-accent"></span>
|
<span class="section-accent"></span>
|
||||||
<h2 class="section-title">Sobre Nosotros</h2>
|
<h2 class="section-title">Acerca de la Actividad</h2>
|
||||||
</div>
|
|
||||||
<p class="biz-desc">{{ businessForm.description || 'Aquí aparecerá la descripción del negocio. Un lugar espectacular para disfrutar de lo mejor en compañía de amigos y familiares.' }}</p>
|
|
||||||
|
|
||||||
<!-- Social links -->
|
|
||||||
<div class="social-links mt-4">
|
|
||||||
<button class="social-btn social-wa" v-if="businessForm.whatsapp">
|
|
||||||
<span class="material-icons">chat</span> WhatsApp
|
|
||||||
</button>
|
|
||||||
<button class="social-btn social-ig" v-if="businessForm.instagram">
|
|
||||||
<span class="material-icons">photo_camera</span> Instagram
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="biz-desc">{{ businessForm.description || 'Aquí aparecerá la descripción de la actividad. Un lugar espectacular o una experiencia inolvidable.' }}</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Gallery carousel preview -->
|
<!-- Gallery carousel preview (now second) -->
|
||||||
<section v-if="galleryPreviews.length > 0" class="biz-section">
|
<section v-if="galleryPreviews.length > 0" class="biz-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<span class="section-accent"></span>
|
<span class="section-accent"></span>
|
||||||
@ -380,6 +389,24 @@ const catEmoji = computed(() => CATEGORY_EMOJI[businessForm.value.category || ''
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="biz-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-accent"></span>
|
||||||
|
<h2 class="section-title">{{ businessForm.supplier_name || 'Proveedor / Operador' }}</h2>
|
||||||
|
</div>
|
||||||
|
<p class="biz-desc mb-4" v-if="businessForm.supplier_description">{{ businessForm.supplier_description }}</p>
|
||||||
|
|
||||||
|
<!-- Social links -->
|
||||||
|
<div class="social-links mt-4">
|
||||||
|
<button class="social-btn social-wa" v-if="businessForm.whatsapp">
|
||||||
|
<span class="material-icons">chat</span> WhatsApp
|
||||||
|
</button>
|
||||||
|
<button class="social-btn social-ig" v-if="businessForm.instagram">
|
||||||
|
<span class="material-icons">photo_camera</span> Instagram
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Sticky CTA -->
|
<!-- Sticky CTA -->
|
||||||
<div class="cta-bar">
|
<div class="cta-bar">
|
||||||
<button class="cta-map"><span class="material-icons">near_me</span> Ver en el Mapa</button>
|
<button class="cta-map"><span class="material-icons">near_me</span> Ver en el Mapa</button>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { businessService } from '@/services/businessService'
|
import { businessService } from '@/services/businessService'
|
||||||
@ -24,7 +24,6 @@ const carouselIndex = ref(0)
|
|||||||
const galleryImages = computed(() => {
|
const galleryImages = computed(() => {
|
||||||
if (!business.value) return []
|
if (!business.value) return []
|
||||||
const imgs: string[] = []
|
const imgs: string[] = []
|
||||||
if (business.value.image_url) imgs.push(business.value.image_url)
|
|
||||||
if (business.value.gallery_images?.length) imgs.push(...business.value.gallery_images)
|
if (business.value.gallery_images?.length) imgs.push(...business.value.gallery_images)
|
||||||
return imgs
|
return imgs
|
||||||
})
|
})
|
||||||
@ -131,7 +130,7 @@ const quickInfoPills = computed(() => {
|
|||||||
|
|
||||||
// Social networks available
|
// Social networks available
|
||||||
const hasSocials = computed(() =>
|
const hasSocials = computed(() =>
|
||||||
!!(business.value?.whatsapp || business.value?.instagram || business.value?.facebook || business.value?.latitude)
|
!!(business.value?.whatsapp || business.value?.instagram || business.value?.facebook || business.value?.latitude || business.value?.supplier_name || business.value?.supplier_description)
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -191,10 +190,18 @@ const hasSocials = computed(() =>
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ══════════════════════════════════════════ -->
|
<!-- ══════════════════════════════════════════ -->
|
||||||
<!-- SECTION 2 · QUICK INFO PILLS -->
|
<!-- SECTION 2 · ACTIVITY DETAILS (ABOUT) -->
|
||||||
<!-- ══════════════════════════════════════════ -->
|
<!-- ══════════════════════════════════════════ -->
|
||||||
<div class="pills-scroll-wrap">
|
<section v-if="business.description || quickInfoPills.length" class="biz-section">
|
||||||
<div class="pills-row">
|
<div v-if="business.description" class="section-header" style="margin-bottom: 12px;">
|
||||||
|
<span class="section-accent"></span>
|
||||||
|
<h2 class="section-title">{{ t('business.about') }}</h2>
|
||||||
|
</div>
|
||||||
|
<p v-if="business.description" class="about-text" style="font-size: 1rem; margin-bottom: 24px;">{{ business.description }}</p>
|
||||||
|
|
||||||
|
<!-- QUICK INFO PILLS -->
|
||||||
|
<div v-if="quickInfoPills.length" class="pills-scroll-wrap" style="padding: 0;">
|
||||||
|
<div class="pills-row" style="flex-wrap: wrap; white-space: normal;">
|
||||||
<a
|
<a
|
||||||
v-for="(pill, i) in quickInfoPills"
|
v-for="(pill, i) in quickInfoPills"
|
||||||
:key="i"
|
:key="i"
|
||||||
@ -208,6 +215,7 @@ const hasSocials = computed(() =>
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- ══════════════════════════════════════════ -->
|
<!-- ══════════════════════════════════════════ -->
|
||||||
<!-- SECTION 3 · IMAGE CAROUSEL -->
|
<!-- SECTION 3 · IMAGE CAROUSEL -->
|
||||||
@ -259,25 +267,20 @@ const hasSocials = computed(() =>
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ══════════════════════════════════════════ -->
|
|
||||||
<!-- SECTION 4 · ABOUT -->
|
|
||||||
<!-- ══════════════════════════════════════════ -->
|
|
||||||
<section v-if="business.description" class="biz-section">
|
|
||||||
<div class="section-header">
|
|
||||||
<span class="section-accent"></span>
|
|
||||||
<h2 class="section-title">{{ t('business.about') }}</h2>
|
|
||||||
</div>
|
|
||||||
<p class="about-text">{{ business.description }}</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ══════════════════════════════════════════ -->
|
<!-- ══════════════════════════════════════════ -->
|
||||||
<!-- SECTION 5 · SOCIALS & CONTACT -->
|
<!-- SECTION 5 · SOCIALS & CONTACT -->
|
||||||
<!-- ══════════════════════════════════════════ -->
|
<!-- ══════════════════════════════════════════ -->
|
||||||
<section v-if="hasSocials" class="biz-section">
|
<section v-if="hasSocials" class="biz-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<span class="section-accent"></span>
|
<span class="section-accent"></span>
|
||||||
<h2 class="section-title">{{ t('business.contactUs') }}</h2>
|
<h2 class="section-title">{{ business.supplier_name || t('business.contactUs') }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- QUIÉNES SOMOS DEL PROVEEDOR -->
|
||||||
|
<p v-if="business.supplier_description" class="about-text" style="font-size: 0.95rem; margin-bottom: 24px;">
|
||||||
|
{{ business.supplier_description }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="socials-grid">
|
<div class="socials-grid">
|
||||||
|
|
||||||
<!-- WhatsApp -->
|
<!-- WhatsApp -->
|
||||||
|
|||||||
@ -212,14 +212,14 @@ function resetFilters() {
|
|||||||
:title="t('discover.auth.title')"
|
:title="t('discover.auth.title')"
|
||||||
:message="t('discover.auth.message')"
|
:message="t('discover.auth.message')"
|
||||||
>
|
>
|
||||||
<TransitionGroup v-if="filteredBusinesses.length > 0" name="fade" tag="div" class="biz-grid">
|
<TransitionGroup v-if="filteredBusinesses.length > 0" name="fade" tag="div" class="activity-grid">
|
||||||
<div v-for="biz in filteredBusinesses" :key="biz.id" class="biz-card" @click="handleExplore(biz)">
|
<div v-for="biz in filteredBusinesses" :key="biz.id" class="activity-card" @click="handleExplore(biz)">
|
||||||
<div class="biz-img-wrap">
|
<div class="card-img-wrap">
|
||||||
<img :src="getImageUrl(biz.image_url, 'business')" :alt="biz.name" loading="lazy" class="biz-img"
|
<img :src="getImageUrl(biz.image_url, 'business')" :alt="biz.name" loading="lazy" class="card-img"
|
||||||
@error="(e) => (e.target as HTMLImageElement).src = getImageUrl(null, 'business')" />
|
@error="(e) => (e.target as HTMLImageElement).src = getImageUrl(null, 'business')" />
|
||||||
</div>
|
</div>
|
||||||
<div class="biz-body">
|
<div class="card-info">
|
||||||
<p class="biz-name">{{ biz.name }}</p>
|
<p class="card-title">{{ biz.name }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
@ -241,13 +241,14 @@ function resetFilters() {
|
|||||||
<!-- Destacados -->
|
<!-- Destacados -->
|
||||||
<div v-if="featuredBusinesses.length > 0" class="featured-section">
|
<div v-if="featuredBusinesses.length > 0" class="featured-section">
|
||||||
<p class="section-label">{{ t('discover.sections.featured') }}</p>
|
<p class="section-label">{{ t('discover.sections.featured') }}</p>
|
||||||
<div class="featured-grid">
|
<div class="activity-grid">
|
||||||
<div v-for="biz in featuredBusinesses" :key="biz.id" class="featured-card" @click="handleExplore(biz)">
|
<div v-for="biz in featuredBusinesses" :key="biz.id" class="activity-card" @click="handleExplore(biz)">
|
||||||
<img :src="getImageUrl(biz.image_url, 'business')" :alt="biz.name" loading="lazy" class="featured-img"
|
<div class="card-img-wrap">
|
||||||
|
<img :src="getImageUrl(biz.image_url, 'business')" :alt="biz.name" loading="lazy" class="card-img"
|
||||||
@error="(e) => (e.target as HTMLImageElement).src = getImageUrl(null, 'business')" />
|
@error="(e) => (e.target as HTMLImageElement).src = getImageUrl(null, 'business')" />
|
||||||
<div class="featured-gradient"></div>
|
</div>
|
||||||
<div class="featured-info">
|
<div class="card-info">
|
||||||
<p class="featured-name">{{ biz.name }}</p>
|
<p class="card-title">{{ biz.name }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -256,14 +257,14 @@ function resetFilters() {
|
|||||||
<!-- Todos los lugares -->
|
<!-- Todos los lugares -->
|
||||||
<div v-if="gridBusinesses.length > 0" class="all-section">
|
<div v-if="gridBusinesses.length > 0" class="all-section">
|
||||||
<p class="section-label">{{ t('discover.sections.allPlaces') }}</p>
|
<p class="section-label">{{ t('discover.sections.allPlaces') }}</p>
|
||||||
<TransitionGroup name="fade" tag="div" class="biz-grid">
|
<TransitionGroup name="fade" tag="div" class="activity-grid">
|
||||||
<div v-for="biz in gridBusinesses" :key="biz.id" class="biz-card" @click="handleExplore(biz)">
|
<div v-for="biz in gridBusinesses" :key="biz.id" class="activity-card" @click="handleExplore(biz)">
|
||||||
<div class="biz-img-wrap">
|
<div class="card-img-wrap">
|
||||||
<img :src="getImageUrl(biz.image_url, 'business')" :alt="biz.name" loading="lazy" class="biz-img"
|
<img :src="getImageUrl(biz.image_url, 'business')" :alt="biz.name" loading="lazy" class="card-img"
|
||||||
@error="(e) => (e.target as HTMLImageElement).src = getImageUrl(null, 'business')" />
|
@error="(e) => (e.target as HTMLImageElement).src = getImageUrl(null, 'business')" />
|
||||||
</div>
|
</div>
|
||||||
<div class="biz-body">
|
<div class="card-info">
|
||||||
<p class="biz-name">{{ biz.name }}</p>
|
<p class="card-title">{{ biz.name }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
@ -403,19 +404,62 @@ function resetFilters() {
|
|||||||
|
|
||||||
/* CARDS */
|
/* CARDS */
|
||||||
.featured-section { margin-bottom: 1.5rem; }
|
.featured-section { margin-bottom: 1.5rem; }
|
||||||
.featured-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.75rem; }
|
|
||||||
.featured-card { position: relative; border-radius: 1.125rem; overflow: hidden; cursor: pointer; aspect-ratio: 16/9; background: var(--bg-secondary); }
|
|
||||||
.featured-img { width: 100%; height: 100%; object-fit: cover; }
|
|
||||||
.featured-gradient { position: absolute; inset: 0; background: linear-gradient(to top, rgba(0,0,0,0.7) 0%, transparent 60%); }
|
|
||||||
.featured-info { position: absolute; bottom: 0; left: 0; right: 0; padding: 0.75rem; z-index: 2; text-align: center; }
|
|
||||||
.featured-name { margin: 0; font-size: 0.9375rem; font-weight: 900; color: #ffffff; text-shadow: 0 1px 4px rgba(0,0,0,0.5); }
|
|
||||||
|
|
||||||
.biz-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.75rem; }
|
.activity-grid {
|
||||||
.biz-card { background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 1rem; overflow: hidden; cursor: pointer; }
|
display: grid;
|
||||||
.biz-img-wrap { position: relative; aspect-ratio: 4/3; }
|
grid-template-columns: repeat(2, 1fr);
|
||||||
.biz-img { width: 100%; height: 100%; object-fit: cover; }
|
gap: 0.875rem;
|
||||||
.biz-body { padding: 0.75rem; text-align: center; }
|
}
|
||||||
.biz-name { margin: 0; font-size: 0.875rem; font-weight: 800; color: var(--text-primary); line-height: 1.2; }
|
|
||||||
|
.activity-card {
|
||||||
|
background: #0B132B;
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 12px 30px rgba(0,0,0,0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-img-wrap {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 4/3;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
padding: 0.875rem;
|
||||||
|
text-align: left;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #ffffff;
|
||||||
|
line-height: 1.3;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
/* UTILS */
|
/* UTILS */
|
||||||
.results-bar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; }
|
.results-bar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; }
|
||||||
@ -437,6 +481,6 @@ function resetFilters() {
|
|||||||
.fade-enter-from, .fade-leave-to { opacity: 0; transform: translateY(10px); }
|
.fade-enter-from, .fade-leave-to { opacity: 0; transform: translateY(10px); }
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.biz-grid, .featured-grid { grid-template-columns: repeat(2, 1fr); }
|
.activity-grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user