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>
203 lines
6.7 KiB
Vue
203 lines
6.7 KiB
Vue
<script setup lang="ts">
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const { trips, loading, polling, error, searchMeta, search, stopPolling } = useFlightSearch()
|
|
const { sortBy, filters, viewMode, filtered, availableAirlines, priceRange, hasActiveFilters, resetFilters } = useResultFilters(trips)
|
|
const { create: createTracking } = useTrackedSearches()
|
|
const user = useSupabaseUser()
|
|
|
|
const trackingName = ref('')
|
|
const showTrackingForm = ref(false)
|
|
const creatingTracking = ref(false)
|
|
|
|
const searchMode = computed(() => (route.query.mode as string) || 'roundtrip')
|
|
const showFilters = ref(false)
|
|
|
|
const passengers = computed(() => ({
|
|
adult: Number(route.query.adults) || 1,
|
|
child: Number(route.query.children) || 0,
|
|
infant: Number(route.query.infants) || 0
|
|
}))
|
|
|
|
const multiCityStops = computed(() =>
|
|
(route.query.stops as string)?.split(',').filter(Boolean) || []
|
|
)
|
|
|
|
const searchParams = computed(() => ({
|
|
departures: (route.query.dep as string)?.split(',') || [],
|
|
destination: (route.query.dest as string)?.split(',').filter(Boolean) || [],
|
|
dateFrom: (route.query.from as string) || '',
|
|
dateTo: (route.query.to as string) || '',
|
|
stayMinDays: Number(route.query.smin) || 2,
|
|
stayMaxDays: Number(route.query.smax) || 6,
|
|
passengers: passengers.value,
|
|
maxStops: route.query.maxStops != null ? Number(route.query.maxStops) : null,
|
|
multiCityStops: multiCityStops.value.length > 0 ? multiCityStops.value : undefined
|
|
}))
|
|
|
|
// Apply budget from query as initial filter
|
|
onMounted(() => {
|
|
if (route.query.budget) {
|
|
filters.maxPrice = Number(route.query.budget)
|
|
}
|
|
const hasDestination = searchParams.value.destination.length > 0 || (searchParams.value.multiCityStops && searchParams.value.multiCityStops.length > 0)
|
|
if (searchParams.value.departures.length && hasDestination) {
|
|
search(searchParams.value)
|
|
}
|
|
})
|
|
|
|
function selectTrip(trip: any) {
|
|
router.push({
|
|
path: `/detail/${encodeURIComponent(trip.bookingToken)}`,
|
|
query: {
|
|
adults: String(passengers.value.adult),
|
|
children: String(passengers.value.child),
|
|
infants: String(passengers.value.infant),
|
|
price: String(trip.totalCost)
|
|
}
|
|
})
|
|
}
|
|
|
|
const modeLabels: Record<string, string> = {
|
|
roundtrip: 'Ida y vuelta',
|
|
oneway: 'Solo ida',
|
|
multicity: 'Multi-ciudad',
|
|
weekend: 'Finde'
|
|
}
|
|
|
|
const routeSummary = computed(() => {
|
|
if (multiCityStops.value.length > 0) {
|
|
return `${searchParams.value.departures.join(',')} > ${multiCityStops.value.join(' > ')}`
|
|
}
|
|
return `${searchParams.value.departures.join(',')} > ${searchParams.value.destination.join(',')}`
|
|
})
|
|
|
|
async function onCreateTracking() {
|
|
if (!trackingName.value) return
|
|
creatingTracking.value = true
|
|
try {
|
|
const { buildPayload } = useFlightSearch()
|
|
await createTracking({
|
|
name: trackingName.value,
|
|
searchParams: buildPayload(searchParams.value),
|
|
routeSummary: routeSummary.value,
|
|
intervalHours: 24
|
|
})
|
|
showTrackingForm.value = false
|
|
trackingName.value = ''
|
|
} finally {
|
|
creatingTracking.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<UPageHero
|
|
:title="multiCityStops.length > 0
|
|
? `${searchParams.departures.join(', ')} → ${multiCityStops.join(' → ')}`
|
|
: `${searchParams.departures.join(', ')} → ${searchParams.destination.join(', ')}`"
|
|
:description="`${modeLabels[searchMode] || searchMode} · ${searchParams.dateFrom || 'Flexible'} al ${searchParams.dateTo || 'Flexible'} · ${passengers.adult + passengers.child + passengers.infant} pasajero(s)`"
|
|
/>
|
|
|
|
<UPageSection>
|
|
<div class="max-w-4xl mx-auto space-y-4">
|
|
<ResultsToolbar
|
|
v-model:sort-by="sortBy"
|
|
v-model:view-mode="viewMode"
|
|
:count="filtered.length"
|
|
:has-active-filters="hasActiveFilters"
|
|
@toggle-filters="showFilters = !showFilters"
|
|
@reset-filters="resetFilters"
|
|
/>
|
|
|
|
<!-- Tracking button -->
|
|
<div v-if="user && !loading && filtered.length > 0 && !showTrackingForm" class="flex justify-end">
|
|
<UButton
|
|
label="Hacer seguimiento"
|
|
icon="i-lucide-bell-plus"
|
|
variant="outline"
|
|
size="sm"
|
|
@click="showTrackingForm = true"
|
|
/>
|
|
</div>
|
|
<UCard v-if="showTrackingForm" class="border-primary-200">
|
|
<div class="flex items-end gap-3">
|
|
<UFormField label="Nombre del seguimiento" class="flex-1">
|
|
<UInput v-model="trackingName" :placeholder="`Ej: ${routeSummary}`" class="w-full" />
|
|
</UFormField>
|
|
<UButton
|
|
label="Crear"
|
|
icon="i-lucide-plus"
|
|
size="sm"
|
|
:loading="creatingTracking"
|
|
:disabled="!trackingName"
|
|
@click="onCreateTracking"
|
|
/>
|
|
<UButton
|
|
icon="i-lucide-x"
|
|
color="neutral"
|
|
variant="ghost"
|
|
size="sm"
|
|
@click="showTrackingForm = false"
|
|
/>
|
|
</div>
|
|
</UCard>
|
|
|
|
<!-- Filters panel -->
|
|
<ResultsFilters
|
|
v-if="showFilters"
|
|
v-model:max-price="filters.maxPrice"
|
|
v-model:max-stops="filters.maxStops"
|
|
v-model:airlines="filters.airlines"
|
|
v-model:departure-time-range="filters.departureTimeRange"
|
|
:available-airlines="availableAirlines"
|
|
:price-range="priceRange"
|
|
/>
|
|
|
|
<!-- Loading -->
|
|
<div v-if="loading" class="space-y-4">
|
|
<USkeleton v-for="i in 5" :key="i" class="h-32 w-full" />
|
|
</div>
|
|
|
|
<UAlert v-else-if="error" color="error" icon="i-lucide-alert-circle" :title="error" />
|
|
|
|
<template v-else>
|
|
<div v-if="filtered.length === 0" class="text-center py-12">
|
|
<UIcon name="i-lucide-plane" class="text-4xl text-neutral-300 mb-2" />
|
|
<p class="text-neutral-500">No se encontraron vuelos</p>
|
|
<UButton v-if="hasActiveFilters" label="Limpiar filtros" class="mt-3" variant="outline" @click="resetFilters" />
|
|
</div>
|
|
|
|
<!-- Full view -->
|
|
<template v-if="viewMode === 'full'">
|
|
<TripCard
|
|
v-for="(trip, i) in filtered"
|
|
:key="trip.bookingToken || i"
|
|
:trip="trip"
|
|
@select="selectTrip"
|
|
/>
|
|
</template>
|
|
|
|
<!-- Compact view -->
|
|
<template v-else>
|
|
<ResultsTripCardCompact
|
|
v-for="(trip, i) in filtered"
|
|
:key="trip.bookingToken || i"
|
|
:trip="trip"
|
|
@select="selectTrip"
|
|
/>
|
|
</template>
|
|
</template>
|
|
|
|
<!-- Polling indicator -->
|
|
<ResultsLoadingMore
|
|
:polling="polling"
|
|
:poll-count="searchMeta.pollCount"
|
|
@stop="stopPolling"
|
|
/>
|
|
</div>
|
|
</UPageSection>
|
|
</div>
|
|
</template>
|