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>
165 lines
5.5 KiB
Vue
165 lines
5.5 KiB
Vue
<script setup lang="ts">
|
|
import type { Segment } from '~/server/utils/flightics'
|
|
|
|
const props = defineProps<{
|
|
segment: Segment
|
|
showDivider?: boolean
|
|
}>()
|
|
|
|
const { resolve } = useAirlineNames()
|
|
const { getBookingUrl, getAirlineWebsite } = useBookingUrl()
|
|
|
|
function formatTime(d: string) {
|
|
return new Date(d).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })
|
|
}
|
|
|
|
const segmentDate = computed(() => props.segment.departureDate.slice(0, 10))
|
|
|
|
const segmentLink = computed(() => ({
|
|
path: '/results',
|
|
query: {
|
|
mode: 'oneway',
|
|
dep: props.segment.departureCode,
|
|
dest: props.segment.arrivalCode,
|
|
from: segmentDate.value,
|
|
to: segmentDate.value,
|
|
adults: '1'
|
|
}
|
|
}))
|
|
|
|
const flightCode = computed(() => `${props.segment.company.code}${props.segment.number}`)
|
|
const trackerUrl = computed(() => `https://www.flightradar24.com/data/flights/${flightCode.value.toLowerCase()}`)
|
|
const airlineName = computed(() => resolve(props.segment.company.code, props.segment.company.name))
|
|
|
|
const bookingUrl = computed(() => getBookingUrl({
|
|
airlineCode: props.segment.company.code,
|
|
origin: props.segment.departureCode,
|
|
destination: props.segment.arrivalCode,
|
|
date: props.segment.departureDate
|
|
}))
|
|
const airlineWebsite = computed(() => getAirlineWebsite(props.segment.company.code))
|
|
|
|
// Fetch cheapest price for this specific route
|
|
const segmentPrice = ref<number | null>(null)
|
|
const loadingPrice = ref(false)
|
|
|
|
onMounted(async () => {
|
|
loadingPrice.value = true
|
|
try {
|
|
const date = props.segment.departureDate.slice(0, 10)
|
|
const data = await $fetch<any>('/api/search', {
|
|
method: 'POST',
|
|
body: {
|
|
departures: [props.segment.departureCode],
|
|
local: 'en',
|
|
departureDateInterval: {
|
|
begin: `${date}T00:00:00+00:00`,
|
|
end: `${date}T00:00:00+00:00`
|
|
},
|
|
stops: [{
|
|
locations: [props.segment.arrivalCode],
|
|
stayRange: { min: 0, max: 0 },
|
|
stayDateRange: { begin: '0001-01-01T00:00:00', end: '0001-01-01T00:00:00' },
|
|
continueFromAny: false
|
|
}],
|
|
endInSameLocation: false,
|
|
maxStops: 0,
|
|
fixStopsOrder: false,
|
|
stopLength: { min: 0, max: 0, isSet: false },
|
|
maxResults: 1,
|
|
passengersCount: { adult: 1, child: 0, infant: 0 }
|
|
}
|
|
})
|
|
if (data.trips?.length) {
|
|
segmentPrice.value = data.trips[0].totalCost
|
|
}
|
|
} catch {
|
|
// silently fail
|
|
} finally {
|
|
loadingPrice.value = false
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
class="flex items-center gap-4 py-3 -mx-2 px-2 rounded-lg hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors"
|
|
:class="{ 'border-t border-neutral-100 dark:border-neutral-800': showDivider }"
|
|
>
|
|
<!-- Departure -->
|
|
<div class="text-center w-20">
|
|
<p class="text-xl font-semibold">{{ formatTime(segment.departureDate) }}</p>
|
|
<p class="text-sm font-medium">{{ segment.departureCode }}</p>
|
|
<p class="text-xs text-neutral-400">{{ segment.departureCity }}</p>
|
|
</div>
|
|
|
|
<!-- Flight info -->
|
|
<div class="flex-1 flex flex-col items-center gap-1">
|
|
<div class="flex items-center gap-2">
|
|
<a
|
|
:href="trackerUrl"
|
|
target="_blank"
|
|
class="text-xs font-medium text-neutral-600 dark:text-neutral-300 hover:text-primary-500 transition-colors"
|
|
title="Ver en Flightradar24"
|
|
@click.stop
|
|
>
|
|
<span v-if="airlineName" class="font-medium">{{ airlineName }} · </span>
|
|
<span>{{ segment.company.code }} {{ segment.number }}</span>
|
|
<UIcon name="i-lucide-radar" class="inline ml-0.5 text-[10px] opacity-50" />
|
|
</a>
|
|
</div>
|
|
<div class="w-full h-px bg-neutral-200 dark:bg-neutral-700 relative">
|
|
<UIcon name="i-lucide-plane" class="absolute -top-2 left-1/2 -translate-x-1/2 text-primary-500" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Arrival -->
|
|
<div class="text-center w-20">
|
|
<p class="text-xl font-semibold">{{ formatTime(segment.arrivalDate) }}</p>
|
|
<p class="text-sm font-medium">{{ segment.arrivalCode }}</p>
|
|
<p class="text-xs text-neutral-400">{{ segment.arrivalCity }}</p>
|
|
</div>
|
|
|
|
<!-- Price (links to one-way search) -->
|
|
<NuxtLink :to="segmentLink" class="shrink-0 text-right w-16 hover:opacity-80 transition-opacity" @click.stop>
|
|
<template v-if="loadingPrice">
|
|
<USkeleton class="h-5 w-12 ml-auto" />
|
|
</template>
|
|
<template v-else-if="segmentPrice != null">
|
|
<p class="text-sm font-bold text-primary-600 dark:text-primary-400">
|
|
{{ segmentPrice.toFixed(0) }}€
|
|
</p>
|
|
<p class="text-xs text-muted">solo ida</p>
|
|
</template>
|
|
</NuxtLink>
|
|
</div>
|
|
|
|
<!-- Flight tracker (expandable) -->
|
|
<div class="pl-24 -mt-1 mb-1">
|
|
<DetailFlightTracker :flight-code="flightCode" />
|
|
</div>
|
|
|
|
<!-- Booking links -->
|
|
<div class="pl-24 -mt-1 mb-2 flex items-center gap-2">
|
|
<a
|
|
:href="bookingUrl"
|
|
target="_blank"
|
|
class="inline-flex items-center gap-1 text-xs font-medium text-primary-600 dark:text-primary-400 hover:underline"
|
|
@click.stop
|
|
>
|
|
<UIcon name="i-lucide-ticket" class="text-sm" />
|
|
Reservar vuelo
|
|
</a>
|
|
<a
|
|
v-if="airlineWebsite"
|
|
:href="airlineWebsite"
|
|
target="_blank"
|
|
class="inline-flex items-center gap-1 text-xs text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300"
|
|
@click.stop
|
|
>
|
|
<UIcon name="i-lucide-globe" class="text-sm" />
|
|
Web de {{ airlineName || segment.company.code }}
|
|
</a>
|
|
</div>
|
|
</template>
|