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>
118 lines
3.7 KiB
Vue
118 lines
3.7 KiB
Vue
<script setup lang="ts">
|
|
import type { Trip } from '~/server/utils/flightics'
|
|
|
|
const route = useRoute()
|
|
const { getDetail } = useFlightSearch()
|
|
|
|
const token = computed(() => route.params.token as string)
|
|
const originalPrice = computed(() => Number(route.query.price) || 0)
|
|
const passengers = computed(() => ({
|
|
adult: Number(route.query.adults) || 1,
|
|
child: Number(route.query.children) || 0,
|
|
infant: Number(route.query.infants) || 0
|
|
}))
|
|
|
|
const trip = ref<Trip | null>(null)
|
|
const loading = ref(true)
|
|
const error = ref<string | null>(null)
|
|
|
|
const routeSummary = computed(() => {
|
|
if (!trip.value) return ''
|
|
const legs = trip.value.legs
|
|
const codes = legs.map(l => l.segments[0]?.departureCode).filter(Boolean)
|
|
const lastArr = legs[legs.length - 1]?.segments.at(-1)?.arrivalCode
|
|
if (lastArr) codes.push(lastArr)
|
|
return codes.join(' > ')
|
|
})
|
|
|
|
const departureCode = computed(() => trip.value?.legs[0]?.segments[0]?.departureCode ?? '')
|
|
const arrivalCode = computed(() => trip.value?.legs[0]?.segments.at(-1)?.arrivalCode ?? '')
|
|
const departureDate = computed(() => trip.value?.legs[0]?.segments[0]?.departureDate ?? '')
|
|
|
|
useSeoMeta({ title: () => `Vuelato - ${routeSummary.value || 'Detalle'}` })
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
const data = await getDetail(token.value, passengers.value)
|
|
trip.value = data.trip
|
|
} catch (e: any) {
|
|
error.value = e?.data?.message || 'Error loading detail'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<UPageSection>
|
|
<div class="max-w-3xl mx-auto">
|
|
<UButton label="Volver" icon="i-lucide-arrow-left" variant="ghost" class="mb-4" @click="$router.back()" />
|
|
|
|
<div v-if="loading" class="space-y-4">
|
|
<USkeleton class="h-48 w-full" />
|
|
<USkeleton class="h-48 w-full" />
|
|
</div>
|
|
|
|
<UAlert v-else-if="error" color="error" icon="i-lucide-alert-circle" :title="error" />
|
|
|
|
<template v-else-if="trip">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 class="text-2xl font-bold">{{ routeSummary }}</h1>
|
|
<p class="text-sm text-muted">
|
|
{{ passengers.adult + passengers.child + passengers.infant }} pasajero(s)
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<div class="text-right">
|
|
<p class="text-3xl font-bold text-primary-600 dark:text-primary-400">
|
|
{{ originalPrice.toFixed(0) }}€
|
|
</p>
|
|
</div>
|
|
<DetailWatchlistToggle
|
|
:booking-token="token"
|
|
:route-summary="routeSummary"
|
|
:departure-code="departureCode"
|
|
:arrival-code="arrivalCode"
|
|
:departure-date="departureDate"
|
|
:price="originalPrice"
|
|
:passengers="passengers"
|
|
/>
|
|
<DetailShareButton :title="routeSummary" :price="originalPrice" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Itinerary -->
|
|
<DetailItineraryTimeline :trip="trip" />
|
|
|
|
<!-- Price verifier -->
|
|
<div class="mt-6">
|
|
<DetailPriceVerifier
|
|
:booking-token="token"
|
|
:original-price="originalPrice"
|
|
:passengers="passengers"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Booking CTA -->
|
|
<div v-if="trip.deepLink" class="mt-4">
|
|
<UButton
|
|
:to="trip.deepLink"
|
|
target="_blank"
|
|
label="Reservar en aerolinea"
|
|
icon="i-lucide-external-link"
|
|
size="lg"
|
|
block
|
|
/>
|
|
</div>
|
|
|
|
<!-- Related flights -->
|
|
<div v-if="departureCode && arrivalCode" class="mt-6">
|
|
<DetailRelatedFlights :from="departureCode" :to="arrivalCode" />
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</UPageSection>
|
|
</template>
|