Some checks failed
ci / ci (22, ubuntu-latest) (push) Has been cancelled
Nuxt 4 + Supabase + Flightics API. Incluye búsqueda de vuelos, inspiraciones, watchlist, tracking de precios y mapa interactivo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
198 lines
5.5 KiB
Vue
198 lines
5.5 KiB
Vue
<script setup lang="ts">
|
|
import type { InspirationItem, MultiCityInspirationItem } from '~/server/utils/flightics'
|
|
|
|
const router = useRouter()
|
|
const user = useSupabaseUser()
|
|
const { fetchInspirations } = useFlightSearch()
|
|
const { searches, saveSearch } = useRecentSearches()
|
|
const { homeAirports } = useUserPreferences()
|
|
|
|
const inspirations = ref<InspirationItem[]>([])
|
|
const inspirationFrom = ref('MAD')
|
|
const loadingInspirations = ref(false)
|
|
|
|
// Multi-city inspirations
|
|
const multiCityItems = ref<MultiCityInspirationItem[]>([])
|
|
const loadingMultiCity = ref(false)
|
|
|
|
async function loadInspirations() {
|
|
loadingInspirations.value = true
|
|
try {
|
|
const data = await fetchInspirations(inspirationFrom.value)
|
|
inspirations.value = data.items || []
|
|
} catch {
|
|
inspirations.value = []
|
|
} finally {
|
|
loadingInspirations.value = false
|
|
}
|
|
}
|
|
|
|
async function loadMultiCity() {
|
|
const codes = quickAirports.value.slice(0, 3)
|
|
if (codes.length === 0) return
|
|
loadingMultiCity.value = true
|
|
try {
|
|
const data = await $fetch<any>('/api/multi-city-inspirations', {
|
|
method: 'POST',
|
|
body: { startLocationsCodes: codes, locale: 'en', take: 8 }
|
|
})
|
|
multiCityItems.value = data.items || []
|
|
} catch {
|
|
multiCityItems.value = []
|
|
} finally {
|
|
loadingMultiCity.value = false
|
|
}
|
|
}
|
|
|
|
// Use home airports from profile, fallback to defaults
|
|
const quickAirports = computed(() => {
|
|
if (homeAirports.value.length) return homeAirports.value
|
|
return ['MAD', 'BCN', 'AGP', 'SVQ', 'VLC', 'PMI']
|
|
})
|
|
|
|
// Set initial origin from user preferences
|
|
watch(homeAirports, (airports) => {
|
|
if (airports.length) inspirationFrom.value = airports[0]
|
|
}, { immediate: true })
|
|
|
|
onMounted(() => {
|
|
loadInspirations()
|
|
loadMultiCity()
|
|
})
|
|
|
|
function onSearch(data: any) {
|
|
const dep = data.departures.join(',')
|
|
const dest = data.destination.join(',')
|
|
const summary = dest ? `${dep} > ${dest}` : `${dep} > Explorar`
|
|
|
|
if (user.value) saveSearch(data, summary, data.mode)
|
|
|
|
if (data.mode === 'explore') {
|
|
router.push({ path: '/explore', query: { dep, budget: data.budget } })
|
|
return
|
|
}
|
|
|
|
router.push({
|
|
path: '/results',
|
|
query: {
|
|
mode: data.mode,
|
|
dep,
|
|
dest,
|
|
from: data.dateFrom,
|
|
to: data.dateTo,
|
|
smin: data.stayMinDays,
|
|
smax: data.stayMaxDays,
|
|
adults: data.passengers.adult,
|
|
children: data.passengers.child,
|
|
infants: data.passengers.infant,
|
|
maxStops: data.maxStops ?? undefined,
|
|
budget: data.budget ?? undefined
|
|
}
|
|
})
|
|
}
|
|
|
|
function replaySearch(s: any) {
|
|
const p = s.search_params
|
|
router.push({
|
|
path: '/results',
|
|
query: {
|
|
mode: p.mode || s.search_mode,
|
|
dep: p.departures?.join(','),
|
|
dest: Array.isArray(p.destination) ? p.destination.join(',') : p.destination,
|
|
from: p.dateFrom,
|
|
to: p.dateTo,
|
|
smin: p.stayMinDays,
|
|
smax: p.stayMaxDays,
|
|
adults: p.passengers?.adult,
|
|
children: p.passengers?.child,
|
|
infants: p.passengers?.infant
|
|
}
|
|
})
|
|
}
|
|
|
|
function goToRoute(iata: string) {
|
|
router.push(`/route/${inspirationFrom.value}-${iata}`)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<UPageHero
|
|
title="Vuelato"
|
|
description="Busca vuelos baratos con fechas flexibles"
|
|
/>
|
|
|
|
<!-- Search -->
|
|
<UPageSection>
|
|
<UCard class="max-w-2xl mx-auto">
|
|
<SearchForm compact @search="onSearch" />
|
|
</UCard>
|
|
</UPageSection>
|
|
|
|
<!-- Recent searches -->
|
|
<UPageSection v-if="user && searches.length" title="Busquedas recientes">
|
|
<div class="flex gap-2 flex-wrap">
|
|
<UButton
|
|
v-for="s in searches.slice(0, 5)"
|
|
:key="s.id"
|
|
:label="s.route_summary"
|
|
icon="i-lucide-history"
|
|
color="neutral"
|
|
variant="soft"
|
|
size="sm"
|
|
@click="replaySearch(s)"
|
|
/>
|
|
</div>
|
|
</UPageSection>
|
|
|
|
<!-- Inspirations -->
|
|
<UPageSection title="Vuelos baratos" description="Los mejores precios desde tu aeropuerto">
|
|
<div class="flex gap-2 mb-4 flex-wrap">
|
|
<UButton
|
|
v-for="apt in quickAirports"
|
|
:key="apt"
|
|
:label="apt"
|
|
:color="inspirationFrom === apt ? 'primary' : 'neutral'"
|
|
:variant="inspirationFrom === apt ? 'solid' : 'outline'"
|
|
size="sm"
|
|
@click="inspirationFrom = apt; loadInspirations()"
|
|
/>
|
|
</div>
|
|
|
|
<div v-if="loadingInspirations" class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
<USkeleton v-for="i in 12" :key="i" class="h-16" />
|
|
</div>
|
|
|
|
<InspirationGrid v-else :items="inspirations" :from="inspirationFrom" />
|
|
</UPageSection>
|
|
|
|
<!-- Budget explorer -->
|
|
<UPageSection>
|
|
<InspirationBudgetExplorer
|
|
:items="inspirations"
|
|
:from="inspirationFrom"
|
|
:loading="loadingInspirations"
|
|
@select="goToRoute"
|
|
/>
|
|
</UPageSection>
|
|
|
|
<!-- Multi-city carousel -->
|
|
<UPageSection title="Inspiracion multi-ciudad" description="Itinerarios con varias paradas">
|
|
<div v-if="loadingMultiCity" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
|
<USkeleton v-for="i in 4" :key="i" class="h-20" />
|
|
</div>
|
|
<div v-else-if="multiCityItems.length" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
|
<InspirationMultiCityCard
|
|
v-for="(item, i) in multiCityItems"
|
|
:key="i"
|
|
:item="item"
|
|
@select="router.push('/multi-city')"
|
|
/>
|
|
</div>
|
|
<div v-else class="text-center py-6">
|
|
<UButton to="/multi-city" label="Explorar multi-ciudad" variant="outline" icon="i-lucide-route" />
|
|
</div>
|
|
</UPageSection>
|
|
</div>
|
|
</template>
|