2026-02-21 09:53:31 -05:00
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ref, onMounted, computed } from 'vue';
|
|
|
|
|
import { businessService } from '@/services/businessService';
|
|
|
|
|
import { API_URL } from '@/services/apiClient';
|
|
|
|
|
import type { Business } from '@/types';
|
|
|
|
|
import { useI18n } from 'vue-i18n';
|
|
|
|
|
import { useRouter } from 'vue-router';
|
|
|
|
|
import FavoriteButton from '@/components/FavoriteButton.vue';
|
2026-02-22 15:05:59 -05:00
|
|
|
import { analyticsService } from '@/services/analyticsService';
|
2026-02-21 09:53:31 -05:00
|
|
|
|
|
|
|
|
const { t } = useI18n();
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const businesses = ref<Business[]>([]);
|
|
|
|
|
const isLoading = ref(true);
|
|
|
|
|
const selectedArea = ref('Todas');
|
|
|
|
|
const selectedCategory = ref('Todas');
|
2026-02-22 15:05:59 -05:00
|
|
|
const searchQuery = ref('');
|
2026-02-21 09:53:31 -05:00
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
analyticsService.logEvent({ event_name: 'screen_view', screen_name: 'Discover' });
|
|
|
|
|
try {
|
|
|
|
|
businesses.value = await businessService.getAllBusinesses();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error loading tourist spots:', error);
|
|
|
|
|
} finally {
|
|
|
|
|
isLoading.value = false;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function handleExplore(biz: Business) {
|
|
|
|
|
analyticsService.logEvent({
|
|
|
|
|
event_name: 'promo_click',
|
|
|
|
|
item_id: biz.name,
|
|
|
|
|
properties: { business_id: biz.id }
|
|
|
|
|
});
|
|
|
|
|
router.push('/business/' + biz.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const filteredBusinesses = computed(() => {
|
|
|
|
|
let filtered = businesses.value;
|
|
|
|
|
|
|
|
|
|
if (selectedArea.value !== 'Todas') {
|
|
|
|
|
filtered = filtered.filter(b => b.area === selectedArea.value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (selectedCategory.value !== 'Todas') {
|
|
|
|
|
filtered = filtered.filter(b => b.category === selectedCategory.value);
|
|
|
|
|
}
|
2026-02-22 15:05:59 -05:00
|
|
|
|
|
|
|
|
if (searchQuery.value.trim() !== '') {
|
|
|
|
|
const query = searchQuery.value.toLowerCase();
|
|
|
|
|
filtered = filtered.filter(b =>
|
|
|
|
|
b.name.toLowerCase().includes(query) ||
|
|
|
|
|
(b.area && b.area.toLowerCase().includes(query)) ||
|
|
|
|
|
(b.category && b.category.toLowerCase().includes(query))
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-02-21 09:53:31 -05:00
|
|
|
|
|
|
|
|
return filtered;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const categories = computed<string[]>(() => {
|
|
|
|
|
const cats = new Set(businesses.value.map(b => b.category).filter(Boolean) as string[]);
|
|
|
|
|
return ['Todas', ...Array.from(cats)];
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-22 15:05:59 -05:00
|
|
|
const areas = computed<string[]>(() => {
|
|
|
|
|
const ars = new Set(businesses.value.map(b => b.area).filter(Boolean) as string[]);
|
|
|
|
|
return ['Todas', ...Array.from(ars)];
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-21 09:53:31 -05:00
|
|
|
function getImageUrl(path: string | null | undefined) {
|
|
|
|
|
if (!path) return '/default-business.jpg';
|
|
|
|
|
if (path.startsWith('http')) return path;
|
|
|
|
|
return `${API_URL}${path.startsWith('/') ? '' : '/'}${path}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getCategoryIcon(category: string) {
|
|
|
|
|
const icons: Record<string, string> = {
|
|
|
|
|
'Restaurante': 'restaurant',
|
|
|
|
|
'Turismo': 'landscape',
|
|
|
|
|
'Bebidas': 'local_bar',
|
|
|
|
|
'Comercio': 'store',
|
|
|
|
|
'Hotel': 'hotel',
|
|
|
|
|
'Café': 'local_cafe'
|
|
|
|
|
};
|
|
|
|
|
return icons[category] || 'place';
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-02-22 15:05:59 -05:00
|
|
|
<div class="discover-view bg-white dark:bg-zinc-950 text-slate-900 dark:text-gray-200 min-h-screen pb-32">
|
|
|
|
|
<!-- Header Area -->
|
|
|
|
|
<header class="bg-white/80 dark:bg-zinc-950/80 backdrop-blur-md px-6 pt-12 pb-4 sticky top-0 z-50 border-b border-slate-100 dark:border-zinc-800">
|
|
|
|
|
<div class="flex justify-between items-center mb-2">
|
|
|
|
|
<button @click="router.back()" class="material-icons text-3xl font-bold cursor-pointer dark:text-white">arrow_back</button>
|
|
|
|
|
<h1 class="text-2xl font-extrabold tracking-tighter dark:text-white uppercase italic">SIBU</h1>
|
|
|
|
|
<div class="w-8"></div>
|
2026-02-21 09:53:31 -05:00
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
2026-02-22 15:05:59 -05:00
|
|
|
<main class="px-5 pt-6">
|
|
|
|
|
<!-- Search Bar -->
|
|
|
|
|
<div class="mb-6">
|
|
|
|
|
<div class="relative flex items-center bg-slate-50 dark:bg-zinc-900 rounded-2xl shadow-sm p-1 border border-slate-100 dark:border-zinc-800">
|
|
|
|
|
<div class="p-3">
|
|
|
|
|
<span class="material-icons text-slate-400 dark:text-gray-500">search</span>
|
2026-02-21 09:53:31 -05:00
|
|
|
</div>
|
2026-02-22 15:05:59 -05:00
|
|
|
<input
|
|
|
|
|
v-model="searchQuery"
|
|
|
|
|
class="w-full bg-transparent border-none focus:ring-0 text-slate-900 dark:text-gray-200 font-medium placeholder:text-slate-400 dark:placeholder:text-gray-500 text-[15px]"
|
|
|
|
|
:placeholder="t('discover.searchPlaceholder') || 'Explora los mejores lugares en Chiriquí'"
|
|
|
|
|
type="text"
|
|
|
|
|
/>
|
2026-02-21 09:53:31 -05:00
|
|
|
</div>
|
2026-02-22 15:05:59 -05:00
|
|
|
</div>
|
2026-02-21 09:53:31 -05:00
|
|
|
|
2026-02-22 15:05:59 -05:00
|
|
|
<!-- Region & Category Selectors -->
|
|
|
|
|
<div class="flex gap-3 mb-8">
|
|
|
|
|
<div class="flex-1 bg-slate-50 dark:bg-zinc-900 p-4 rounded-2xl shadow-sm border border-slate-100 dark:border-zinc-800 flex flex-col gap-1">
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<span class="material-icons text-blue-500 text-[18px] font-bold">location_on</span>
|
|
|
|
|
<span class="text-[10px] font-bold text-slate-400 dark:text-gray-500 uppercase tracking-widest">Región</span>
|
2026-02-21 09:53:31 -05:00
|
|
|
</div>
|
2026-02-22 15:05:59 -05:00
|
|
|
<select v-model="selectedArea" class="bg-transparent border-none focus:ring-0 p-0 font-bold text-slate-900 dark:text-white text-sm appearance-none cursor-pointer">
|
|
|
|
|
<option value="Todas">{{ t('discover.allAreas') }}</option>
|
|
|
|
|
<option v-for="area in areas" :key="area" :value="area">{{ area }}</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex-1 bg-slate-50 dark:bg-zinc-900 p-4 rounded-2xl shadow-sm border border-slate-100 dark:border-zinc-800 flex flex-col gap-1">
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<span class="material-icons text-blue-500 text-[18px] font-bold">category</span>
|
|
|
|
|
<span class="text-[10px] font-bold text-slate-400 dark:text-gray-500 uppercase tracking-widest">Categoría</span>
|
2026-02-21 09:53:31 -05:00
|
|
|
</div>
|
2026-02-22 15:05:59 -05:00
|
|
|
<select v-model="selectedCategory" class="bg-transparent border-none focus:ring-0 p-0 font-bold text-slate-900 dark:text-white text-sm appearance-none cursor-pointer">
|
|
|
|
|
<option v-for="cat in categories" :key="cat" :value="cat">{{ cat }}</option>
|
|
|
|
|
</select>
|
2026-02-21 09:53:31 -05:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-22 15:05:59 -05:00
|
|
|
<!-- Loading State -->
|
|
|
|
|
<div v-if="isLoading" class="flex flex-col items-center justify-center py-20">
|
|
|
|
|
<div class="w-10 h-10 border-4 border-blue-500/20 border-t-blue-500 rounded-full animate-spin mb-4"></div>
|
|
|
|
|
<p class="text-slate-500 dark:text-gray-400 font-bold tracking-widest uppercase text-xs">Sincronizando con SIBU...</p>
|
2026-02-21 09:53:31 -05:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Empty State -->
|
2026-02-22 15:05:59 -05:00
|
|
|
<div v-else-if="filteredBusinesses.length === 0" class="text-center py-20">
|
|
|
|
|
<div class="bg-slate-50 dark:bg-zinc-900 rounded-[2rem] p-10 border border-dashed border-slate-200 dark:border-zinc-800">
|
|
|
|
|
<span class="material-icons text-slate-300 dark:text-gray-600 text-5xl mb-4">search_off</span>
|
|
|
|
|
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-2">Sin resultados</h3>
|
|
|
|
|
<p class="text-slate-500 dark:text-gray-400 mb-6">La búsqueda no devolvió datos en esta frecuencia.</p>
|
|
|
|
|
<button @click="selectedArea = 'Todas'; selectedCategory = 'Todas'; searchQuery = ''" class="bg-blue-500 text-white px-8 py-3 rounded-xl font-black uppercase text-xs tracking-widest active:scale-95 transition-all">
|
2026-02-21 09:53:31 -05:00
|
|
|
REINICIAR SENSORES
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-22 15:05:59 -05:00
|
|
|
<!-- Business Grid -->
|
2026-02-21 09:53:31 -05:00
|
|
|
<TransitionGroup
|
|
|
|
|
v-else
|
2026-02-22 15:05:59 -05:00
|
|
|
name="fade"
|
2026-02-21 09:53:31 -05:00
|
|
|
tag="div"
|
2026-02-22 15:05:59 -05:00
|
|
|
class="grid grid-cols-2 gap-4"
|
2026-02-21 09:53:31 -05:00
|
|
|
>
|
2026-02-22 15:05:59 -05:00
|
|
|
<div v-for="biz in filteredBusinesses"
|
2026-02-21 09:53:31 -05:00
|
|
|
:key="biz.id"
|
2026-02-22 15:05:59 -05:00
|
|
|
class="bg-slate-50 dark:bg-zinc-900 rounded-[2rem] overflow-hidden shadow-sm border border-slate-100 dark:border-zinc-800 flex flex-col active:scale-95 transition-all cursor-pointer"
|
2026-02-21 09:53:31 -05:00
|
|
|
@click="handleExplore(biz)"
|
|
|
|
|
>
|
2026-02-22 15:05:59 -05:00
|
|
|
<div class="relative h-44">
|
|
|
|
|
<img :src="getImageUrl(biz.image_url)" alt="" class="w-full h-full object-cover">
|
|
|
|
|
<div class="absolute inset-0 bg-gradient-to-b from-black/40 to-transparent"></div>
|
|
|
|
|
<div class="absolute top-3 left-3 bg-black/60 backdrop-blur-md p-1.5 rounded-xl border border-white/10">
|
|
|
|
|
<span class="material-icons text-blue-400 text-[18px] flex items-center justify-center">
|
|
|
|
|
{{ getCategoryIcon(biz.category || '') }}
|
|
|
|
|
</span>
|
2026-02-21 09:53:31 -05:00
|
|
|
</div>
|
2026-02-22 15:05:59 -05:00
|
|
|
<div class="absolute top-3 right-3">
|
|
|
|
|
<FavoriteButton
|
|
|
|
|
item-type="business"
|
|
|
|
|
:item-id="biz.id"
|
|
|
|
|
:item-name="biz.name"
|
|
|
|
|
:item-image="biz.image_url || undefined"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="p-4 flex flex-col flex-grow">
|
|
|
|
|
<h3 class="font-bold text-slate-900 dark:text-white text-[15px] leading-tight mb-2">{{ biz.name }}</h3>
|
|
|
|
|
<div class="mt-auto flex items-center justify-between">
|
|
|
|
|
<div class="flex items-center gap-1">
|
|
|
|
|
<span class="material-icons text-blue-400 text-[14px]">near_me</span>
|
|
|
|
|
<span class="text-xs font-semibold text-slate-500 dark:text-gray-500">{{ biz.area }}</span>
|
2026-02-21 09:53:31 -05:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</TransitionGroup>
|
|
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.discover-view {
|
2026-02-22 15:05:59 -05:00
|
|
|
font-family: 'Plus Jakarta Sans', sans-serif;
|
2026-02-21 09:53:31 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 15:05:59 -05:00
|
|
|
.fade-move,
|
|
|
|
|
.fade-enter-active,
|
|
|
|
|
.fade-leave-active {
|
2026-02-21 09:53:31 -05:00
|
|
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 15:05:59 -05:00
|
|
|
.fade-enter-from,
|
|
|
|
|
.fade-leave-to {
|
2026-02-21 09:53:31 -05:00
|
|
|
opacity: 0;
|
2026-02-22 15:05:59 -05:00
|
|
|
transform: translateY(20px);
|
2026-02-21 09:53:31 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 15:05:59 -05:00
|
|
|
.fade-leave-active {
|
|
|
|
|
position: absolute;
|
2026-02-21 09:53:31 -05:00
|
|
|
}
|
|
|
|
|
</style>
|