Files
vuelato/app/components/TripCard.vue
Alejandro Martinez b8906efc80
Some checks failed
ci / ci (22, ubuntu-latest) (push) Has been cancelled
Initial commit: Vuelato - buscador de vuelos
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>
2026-04-10 23:37:06 +02:00

134 lines
5.1 KiB
Vue

<script setup lang="ts">
import type { Trip } from '~/server/utils/flightics'
const props = defineProps<{ trip: Trip }>()
defineEmits<{ select: [trip: Trip] }>()
const { showOriginTime } = useOriginTime()
// Timezone offset (in seconds) of the origin airport: local - UTC
const originTzOffset = computed(() => {
const seg = props.trip.legs[0]?.segments[0]
if (!seg) return 0
return seg.departureTimestamp - seg.departureUtcTimestamp
})
const departureCode = computed(() => props.trip.legs[0]?.segments[0]?.departureCode ?? '')
const arrivalCode = computed(() => props.trip.legs[0]?.segments.at(-1)?.arrivalCode ?? '')
const departureDate = computed(() => props.trip.legs[0]?.segments[0]?.departureDate ?? '')
const routeSummary = computed(() => {
const codes = props.trip.legs.map(l => l.segments[0]?.departureCode).filter(Boolean)
const lastArr = props.trip.legs.at(-1)?.segments.at(-1)?.arrivalCode
if (lastArr) codes.push(lastArr)
return codes.join(' > ')
})
// Total flight time across all legs (sum of each segment's flight duration, using UTC)
const totalFlightMs = computed(() => {
let ms = 0
for (const leg of props.trip.legs) {
for (const seg of leg.segments) {
ms += (seg.arrivalUtcTimestamp - seg.departureUtcTimestamp) * 1000
}
}
return ms
})
// Time at destination: from arrival of last segment of outbound leg to departure of first segment of return leg (using UTC)
const stayInfo = computed(() => {
const legs = props.trip.legs
if (legs.length < 2) return null
const arrivalUtc = legs[0].segments.at(-1)?.arrivalUtcTimestamp
const departureUtc = legs[1].segments[0]?.departureUtcTimestamp
if (!arrivalUtc || !departureUtc) return null
const stayMs = (departureUtc - arrivalUtc) * 1000
if (stayMs <= 0) return null
const stayHours = stayMs / 3600000
const fullDays = Math.floor(stayHours / 24)
const remainingHours = Math.round(stayHours - fullDays * 24)
const nights = fullDays
return { nights, fullDays, remainingHours, stayMs }
})
// Total trip days: from first departure to last arrival (local dates for calendar/vacation planning)
const totalTripDays = computed(() => {
const legs = props.trip.legs
if (!legs.length) return null
const firstDep = legs[0].segments[0]?.departureDate
const lastArr = legs.at(-1)?.segments.at(-1)?.arrivalDate
if (!firstDep || !lastArr) return null
const depDate = new Date(firstDep)
const arrDate = new Date(lastArr)
// Calendar days: count from departure day to arrival day inclusive
const depDay = new Date(depDate.getFullYear(), depDate.getMonth(), depDate.getDate())
const arrDay = new Date(arrDate.getFullYear(), arrDate.getMonth(), arrDate.getDate())
const calendarDays = Math.round((arrDay.getTime() - depDay.getTime()) / 86400000) + 1
return calendarDays
})
function formatDuration(ms: number) {
const totalMin = Math.round(ms / 60000)
const h = Math.floor(totalMin / 60)
const m = totalMin % 60
return h > 0 ? `${h}h ${m}m` : `${m}m`
}
</script>
<template>
<UCard class="hover:ring-primary-500 transition-all cursor-pointer" @click="$emit('select', trip)">
<div class="flex items-center justify-between gap-4">
<div class="flex-1 space-y-1">
<FlightLeg v-for="(leg, i) in trip.legs" :key="i" :leg="leg" :index="i" :origin-tz-offset="originTzOffset" :show-origin-time="showOriginTime" />
</div>
<div class="text-right shrink-0 pl-4 border-l border-neutral-200 dark:border-neutral-700 space-y-1">
<p class="text-2xl font-bold text-primary-600 dark:text-primary-400">
{{ trip.totalCost.toFixed(0) }}<span class="text-sm font-normal ml-0.5">&euro;</span>
</p>
<p class="text-xs text-muted flex items-center gap-1 justify-end">
<UIcon name="i-lucide-plane" class="text-xs" />
{{ formatDuration(totalFlightMs) }}
</p>
<template v-if="stayInfo">
<p class="text-xs text-muted flex items-center gap-1 justify-end">
<UIcon name="i-lucide-map-pin" class="text-xs" />
{{ stayInfo.fullDays }}d {{ stayInfo.remainingHours }}h en destino
</p>
<p class="text-xs text-muted flex items-center gap-1 justify-end">
<UIcon name="i-lucide-moon" class="text-xs" />
{{ stayInfo.nights }} noche{{ stayInfo.nights !== 1 ? 's' : '' }}
</p>
</template>
<p v-if="totalTripDays" class="text-xs text-muted flex items-center gap-1 justify-end">
<UIcon name="i-lucide-calendar-range" class="text-xs" />
{{ totalTripDays }} dia{{ totalTripDays !== 1 ? 's' : '' }} total
</p>
<div class="flex items-center gap-1 justify-end pt-1">
<DetailWatchlistToggle
:booking-token="trip.bookingToken"
:route-summary="routeSummary"
:departure-code="departureCode"
:arrival-code="arrivalCode"
:departure-date="departureDate"
:price="trip.totalCost"
:passengers="{ adult: 1, child: 0, infant: 0 }"
/>
<UButton size="xs" label="Ver" trailing-icon="i-lucide-arrow-right" />
</div>
</div>
</div>
</UCard>
</template>