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>
202 lines
6.6 KiB
Vue
202 lines
6.6 KiB
Vue
<script setup lang="ts">
|
|
const user = useSupabaseUser()
|
|
const { items, loading, checkPrice, checkAll, remove } = useWatchlist()
|
|
const { searches } = useRecentSearches()
|
|
const { create: createTracking } = useTrackedSearches()
|
|
const { buildPayload } = useFlightSearch()
|
|
const router = useRouter()
|
|
|
|
const checkingAll = ref(false)
|
|
const checkingId = ref<string | null>(null)
|
|
|
|
useSeoMeta({ title: 'Vuelato - Watchlist' })
|
|
|
|
// Redirect to auth if not logged in
|
|
watch(user, (u) => {
|
|
if (!u) navigateTo('/auth')
|
|
}, { immediate: true })
|
|
|
|
async function onCheckPrice(item: any) {
|
|
checkingId.value = item.id
|
|
await checkPrice(item)
|
|
checkingId.value = null
|
|
}
|
|
|
|
async function onCheckAll() {
|
|
checkingAll.value = true
|
|
await checkAll()
|
|
checkingAll.value = false
|
|
}
|
|
|
|
function statusColor(status: string) {
|
|
switch (status) {
|
|
case 'price_down': return 'success'
|
|
case 'price_up': return 'warning'
|
|
case 'unavailable': return 'error'
|
|
case 'available': return 'info'
|
|
default: return 'neutral'
|
|
}
|
|
}
|
|
|
|
function statusLabel(status: string) {
|
|
switch (status) {
|
|
case 'price_down': return 'Bajo'
|
|
case 'price_up': return 'Subio'
|
|
case 'unavailable': return 'No disponible'
|
|
case 'available': return 'Disponible'
|
|
default: return 'Guardado'
|
|
}
|
|
}
|
|
|
|
function replaySearch(s: any) {
|
|
const p = s.search_params
|
|
router.push({
|
|
path: '/results',
|
|
query: {
|
|
mode: p.mode || s.search_mode,
|
|
dep: p.departures?.join(','),
|
|
dest: Array.isArray(p.destination) ? p.destination.join(',') : p.destination,
|
|
from: p.dateFrom,
|
|
to: p.dateTo,
|
|
smin: p.stayMinDays,
|
|
smax: p.stayMaxDays,
|
|
adults: p.passengers?.adult,
|
|
children: p.passengers?.child,
|
|
infants: p.passengers?.infant
|
|
}
|
|
})
|
|
}
|
|
|
|
async function trackSearch(s: any) {
|
|
const p = s.search_params
|
|
await createTracking({
|
|
name: s.route_summary || 'Busqueda sin nombre',
|
|
searchParams: buildPayload({
|
|
departures: p.departures || [],
|
|
destination: Array.isArray(p.destination) ? p.destination : (p.destination || '').split(',').filter(Boolean),
|
|
dateFrom: p.dateFrom || '',
|
|
dateTo: p.dateTo || '',
|
|
stayMinDays: p.stayMinDays || 2,
|
|
stayMaxDays: p.stayMaxDays || 6,
|
|
passengers: p.passengers || { adult: 1, child: 0, infant: 0 }
|
|
}),
|
|
routeSummary: s.route_summary || 'Sin ruta',
|
|
intervalHours: 24
|
|
})
|
|
navigateTo('/tracking')
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="user">
|
|
<UPageHero title="Watchlist" description="Vuelos guardados y seguimiento de precios" />
|
|
|
|
<UPageSection>
|
|
<div class="max-w-3xl mx-auto space-y-4">
|
|
<!-- Actions -->
|
|
<div class="flex items-center justify-between">
|
|
<p class="text-sm text-muted">{{ items.length }} vuelo{{ items.length !== 1 ? 's' : '' }} guardado{{ items.length !== 1 ? 's' : '' }}</p>
|
|
<UButton
|
|
v-if="items.length > 0"
|
|
label="Verificar todos"
|
|
icon="i-lucide-refresh-cw"
|
|
:loading="checkingAll"
|
|
size="sm"
|
|
variant="outline"
|
|
@click="onCheckAll"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Loading -->
|
|
<div v-if="loading" class="space-y-3">
|
|
<USkeleton v-for="i in 3" :key="i" class="h-24 w-full" />
|
|
</div>
|
|
|
|
<!-- Empty -->
|
|
<div v-else-if="items.length === 0" class="text-center py-12">
|
|
<UIcon name="i-lucide-heart" class="text-4xl text-neutral-300 mb-2" />
|
|
<p class="text-neutral-500">No tienes vuelos guardados</p>
|
|
<UButton to="/search" label="Buscar vuelos" class="mt-3" />
|
|
</div>
|
|
|
|
<!-- Items -->
|
|
<UCard v-for="item in items" :key="item.id">
|
|
<div class="flex items-center justify-between gap-4">
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<p class="font-semibold">{{ item.route_summary }}</p>
|
|
<UBadge :label="statusLabel(item.price_status)" :color="statusColor(item.price_status)" size="xs" />
|
|
</div>
|
|
<div class="flex items-center gap-3 text-sm text-muted">
|
|
<span v-if="item.departure_date">
|
|
{{ new Date(item.departure_date).toLocaleDateString('es-ES', { day: 'numeric', month: 'short' }) }}
|
|
</span>
|
|
<span>Original: {{ item.original_price.toFixed(0) }}€</span>
|
|
<span v-if="item.current_price != null && item.current_price !== item.original_price"
|
|
:class="item.current_price < item.original_price ? 'text-green-600' : 'text-red-500'"
|
|
>
|
|
Actual: {{ item.current_price.toFixed(0) }}€
|
|
</span>
|
|
<span v-if="item.last_checked_at" class="text-xs">
|
|
Verificado {{ new Date(item.last_checked_at).toLocaleDateString('es-ES') }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-1 shrink-0">
|
|
<UButton
|
|
icon="i-lucide-refresh-cw"
|
|
color="neutral"
|
|
variant="ghost"
|
|
size="xs"
|
|
:loading="checkingId === item.id"
|
|
@click="onCheckPrice(item)"
|
|
/>
|
|
<UButton
|
|
:to="`/detail/${encodeURIComponent(item.booking_token)}?price=${item.original_price}&adults=${item.passengers_adult}&children=${item.passengers_child}&infants=${item.passengers_infant}`"
|
|
icon="i-lucide-external-link"
|
|
color="neutral"
|
|
variant="ghost"
|
|
size="xs"
|
|
/>
|
|
<UButton
|
|
icon="i-lucide-trash-2"
|
|
color="error"
|
|
variant="ghost"
|
|
size="xs"
|
|
@click="remove(item.id)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
</div>
|
|
</UPageSection>
|
|
|
|
<!-- Recent searches -->
|
|
<UPageSection v-if="searches.length > 0" title="Busquedas recientes">
|
|
<div class="max-w-3xl mx-auto">
|
|
<div class="flex gap-2 flex-wrap">
|
|
<div v-for="s in searches.slice(0, 8)" :key="s.id" class="flex items-center gap-0.5">
|
|
<UButton
|
|
:label="s.route_summary"
|
|
icon="i-lucide-history"
|
|
color="neutral"
|
|
variant="soft"
|
|
size="sm"
|
|
@click="replaySearch(s)"
|
|
/>
|
|
<UButton
|
|
icon="i-lucide-bell-plus"
|
|
color="neutral"
|
|
variant="ghost"
|
|
size="xs"
|
|
title="Hacer seguimiento"
|
|
@click="trackSearch(s)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</UPageSection>
|
|
</div>
|
|
</template>
|