Initial commit: Vuelato - buscador de vuelos
Some checks failed
ci / ci (22, ubuntu-latest) (push) Has been cancelled
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>
This commit is contained in:
164
app/components/detail/SegmentCard.vue
Normal file
164
app/components/detail/SegmentCard.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user