Initial commit: Vuelato - buscador de vuelos
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:
Alejandro Martinez
2026-04-10 23:37:06 +02:00
commit b8906efc80
122 changed files with 37809 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
interface BookingParams {
airlineCode: string
origin: string
destination: string
date: string // ISO date string
passengers?: number
}
// Direct booking URL templates for major airlines
// date format helpers applied per-airline
const BOOKING_TEMPLATES: Record<string, (p: BookingParams & { d: string; ymd: string }) => string> = {
FR: p => `https://www.ryanair.com/es/es/trip/flights/select?adults=${p.passengers}&teens=0&children=0&infants=0&dateOut=${p.ymd}&originIata=${p.origin}&destinationIata=${p.destination}&isReturn=false`,
U2: p => `https://www.easyjet.com/es/search?origin=${p.origin}&destination=${p.destination}&outboundDate=${p.ymd}&adults=${p.passengers}`,
VY: () => `https://www.vueling.com/es/reserva-tu-vuelo/busca-tu-vuelo`,
LH: p => `https://www.lufthansa.com/es/es/offer/search?origin=${p.origin}&destination=${p.destination}&departureDate=${p.ymd}&paxAdult=${p.passengers}&cabinClass=ECONOMY&tripType=O`,
IB: p => `https://www.iberia.com/es/?language=es&market=ES&origin=${p.origin}&destination=${p.destination}&outbound=${p.ymd}&adults=${p.passengers}&cabin=ECONOMY`,
W6: p => `https://wizzair.com/es-es#/booking/select-flight/${p.origin}/${p.destination}/${p.ymd}/null/${p.passengers}/0/0/null`,
NK: p => `https://www.spirit.com/book/flights?origStation=${p.origin}&destStation=${p.destination}&date=${p.ymd}&adt=${p.passengers}&chd=0&inf=0&promoCode=&tripType=OW`,
KL: p => `https://www.klm.es/search?pax=${p.passengers}:0:0:0:0:0:0:0&cabinClass=ECONOMY&connections=${p.origin}:C%3E${p.destination}:C&bookingFlow=LEISURE`,
AF: p => `https://www.airfrance.es/search?pax=${p.passengers}:0:0:0:0:0:0:0&cabinClass=ECONOMY&connections=${p.origin}:C%3E${p.destination}:C&bookingFlow=LEISURE`,
TK: p => `https://www.turkishairlines.com/es-es/flights/?origin=${p.origin}&destination=${p.destination}&departureDate=${p.ymd}&adult=${p.passengers}&child=0&infant=0&tripType=O`,
AA: p => `https://www.aa.com/booking/find-flights?origin=${p.origin}&destination=${p.destination}&departureDate=${p.ymd}&pax=${p.passengers}&tripType=OneWay&locale=es_ES`,
}
interface AirlineData {
website: string | null
bookingUrl: string | null
bookingUrlTemplate: string | null
}
// Airline data cache (loaded once from API)
const airlineData = ref<Map<string, AirlineData> | null>(null)
let loadingPromise: Promise<void> | null = null
async function loadAirlineData() {
if (airlineData.value) return
if (loadingPromise) return loadingPromise
loadingPromise = $fetch<{ airlines: { iata: string; website: string | null; booking_url: string | null; booking_url_template: string | null }[] }>('/api/airlines')
.then(res => {
const map = new Map<string, AirlineData>()
for (const a of res.airlines) {
map.set(a.iata, {
website: a.website,
bookingUrl: a.booking_url,
bookingUrlTemplate: a.booking_url_template,
})
}
airlineData.value = map
})
.catch(() => {
airlineData.value = new Map()
})
return loadingPromise
}
function applyTemplate(template: string, params: BookingParams, pax: number, ymd: string): string {
return template
.replace(/\{origin\}/gi, params.origin)
.replace(/\{destination\}/gi, params.destination)
.replace(/\{date\}/gi, ymd)
.replace(/\{passengers\}/gi, String(pax))
}
function buildGoogleFlightsUrl(p: BookingParams): string {
const ymd = p.date.slice(0, 10)
return `https://www.google.com/travel/flights?hl=es&curr=EUR&q=flights+from+${p.origin}+to+${p.destination}+on+${ymd}+one+way+${p.passengers}+passenger`
}
export function useBookingUrl() {
// Start loading on first use
loadAirlineData()
function getBookingUrl(params: BookingParams): string {
const pax = params.passengers || 1
const ymd = params.date.slice(0, 10)
const d = ymd.replace(/-/g, '')
// 1. Hardcoded templates (most reliable, manually verified)
const hardcoded = BOOKING_TEMPLATES[params.airlineCode]
if (hardcoded) {
return hardcoded({ ...params, passengers: pax, d, ymd })
}
const data = airlineData.value?.get(params.airlineCode)
// 2. Auto-discovered template with placeholders
if (data?.bookingUrlTemplate) {
return applyTemplate(data.bookingUrlTemplate, params, pax, ymd)
}
// 3. Discovered booking page URL (no params, but lands on the right page)
if (data?.bookingUrl) {
return data.bookingUrl
}
// 4. Google Flights as universal fallback
return buildGoogleFlightsUrl({ ...params, passengers: pax })
}
function getAirlineWebsite(code: string): string | null {
return airlineData.value?.get(code)?.website || null
}
return { getBookingUrl, getAirlineWebsite }
}