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

159
app/pages/tracking/[id].vue Normal file
View File

@@ -0,0 +1,159 @@
<script setup lang="ts">
const user = useSupabaseUser()
const route = useRoute()
const router = useRouter()
const { trackedSearches, getHistory, getRuns, remove, fetchAll, update } = useTrackedSearches()
const id = route.params.id as string
useSeoMeta({ title: 'Vuelato - Detalle seguimiento' })
watch(user, (u) => {
if (!u) navigateTo('/auth')
}, { immediate: true })
const search = computed(() => trackedSearches.value.find(s => s.id === id))
const snapshots = ref<PriceSnapshot[]>([])
const runs = ref<SearchRun[]>([])
const loadingHistory = ref(true)
const loadError = ref(false)
const days = ref(30)
async function loadData() {
loadingHistory.value = true
loadError.value = false
try {
const [h, r] = await Promise.all([
getHistory(id, days.value),
getRuns(id, 20)
])
snapshots.value = h
runs.value = r
} catch {
loadError.value = true
} finally {
loadingHistory.value = false
}
}
watch(days, () => loadData())
onMounted(async () => {
await fetchAll()
loadData()
})
// Stats computadas
const stats = computed(() => {
if (snapshots.value.length === 0) return null
const prices = snapshots.value.map(s => s.cheapest_price)
const min = Math.min(...prices)
const max = Math.max(...prices)
const avg = Math.round(prices.reduce((s, p) => s + p, 0) / prices.length)
const current = prices[prices.length - 1] ?? 0
const previous = prices.length > 1 ? (prices[prices.length - 2] ?? current) : current
const trend = current - previous
return { current, min, max, avg, trend }
})
async function onRemove() {
await remove(id)
router.push('/tracking')
}
async function onConfigUpdated() {
await fetchAll()
loadData()
}
</script>
<template>
<div v-if="user">
<!-- Loading si aun no cargo la search -->
<div v-if="!search" class="max-w-3xl mx-auto py-12">
<USkeleton class="h-8 w-64 mb-4" />
<USkeleton class="h-72 w-full" />
</div>
<template v-else>
<UPageHero :title="search.name" :description="search.route_summary">
<template #actions>
<div class="flex gap-2">
<UButton label="Volver" icon="i-lucide-arrow-left" variant="outline" color="neutral" to="/tracking" />
<UButton label="Eliminar" icon="i-lucide-trash-2" variant="outline" color="error" @click="onRemove" />
</div>
</template>
</UPageHero>
<UPageSection>
<div class="max-w-4xl mx-auto space-y-6">
<!-- Stats -->
<div v-if="stats" class="grid grid-cols-2 sm:grid-cols-4 gap-3">
<UCard class="text-center">
<p class="text-xs text-muted mb-1">Precio actual</p>
<p class="text-xl font-bold">{{ stats.current?.toFixed(0) }}&euro;</p>
<p v-if="stats.trend !== 0" class="text-xs" :class="stats.trend < 0 ? 'text-green-600' : 'text-red-500'">
<UIcon :name="stats.trend < 0 ? 'i-lucide-trending-down' : 'i-lucide-trending-up'" class="text-xs" />
{{ stats.trend > 0 ? '+' : '' }}{{ stats.trend.toFixed(0) }}&euro;
</p>
</UCard>
<UCard class="text-center">
<p class="text-xs text-muted mb-1">Minimo historico</p>
<p class="text-xl font-bold text-green-600">{{ stats.min?.toFixed(0) }}&euro;</p>
</UCard>
<UCard class="text-center">
<p class="text-xs text-muted mb-1">Maximo historico</p>
<p class="text-xl font-bold text-red-500">{{ stats.max?.toFixed(0) }}&euro;</p>
</UCard>
<UCard class="text-center">
<p class="text-xs text-muted mb-1">Precio medio</p>
<p class="text-xl font-bold">{{ stats.avg?.toFixed(0) }}&euro;</p>
</UCard>
</div>
<!-- Chart -->
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h3 class="font-semibold">Evolucion de precios</h3>
<div class="flex gap-1">
<UButton
v-for="d in [7, 14, 30, 60]"
:key="d"
:label="`${d}d`"
size="xs"
:variant="days === d ? 'solid' : 'ghost'"
:color="days === d ? 'primary' : 'neutral'"
@click="days = d"
/>
</div>
</div>
</template>
<div v-if="loadingHistory" class="flex justify-center py-12">
<USkeleton class="h-64 w-full" />
</div>
<div v-else-if="loadError" class="text-center py-8">
<p class="text-sm text-red-500">Error al cargar datos</p>
<UButton label="Reintentar" variant="outline" size="sm" class="mt-2" @click="loadData" />
</div>
<ClientOnly v-else>
<TrackingPriceChart :snapshots="snapshots" />
</ClientOnly>
</UCard>
<!-- Config + Runs side by side on desktop -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div class="lg:col-span-1">
<TrackingConfig :search="search" @updated="onConfigUpdated" />
</div>
<div class="lg:col-span-2">
<TrackingRunHistory :runs="runs" />
</div>
</div>
</div>
</UPageSection>
</template>
</div>
</template>