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:
124
app/components/tracking/TrackedSearchCard.vue
Normal file
124
app/components/tracking/TrackedSearchCard.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
search: {
|
||||
id: string
|
||||
name: string
|
||||
route_summary: string
|
||||
interval_hours: number
|
||||
is_active: boolean
|
||||
next_run_at: string | null
|
||||
last_run_at: string | null
|
||||
run_count: number
|
||||
last_error: string | null
|
||||
expires_at: string | null
|
||||
latest_snapshot: {
|
||||
cheapest_price: number
|
||||
avg_price: number | null
|
||||
total_results: number
|
||||
recorded_at: string
|
||||
} | null
|
||||
}
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
toggle: [id: string, active: boolean]
|
||||
remove: [id: string]
|
||||
}>()
|
||||
|
||||
function statusBadge() {
|
||||
if (!props.search.is_active) return { label: 'Pausada', color: 'neutral' as const }
|
||||
if (props.search.last_error) return { label: 'Error', color: 'error' as const }
|
||||
if (props.search.expires_at && new Date(props.search.expires_at) < new Date()) return { label: 'Expirada', color: 'warning' as const }
|
||||
return { label: 'Activa', color: 'success' as const }
|
||||
}
|
||||
|
||||
function formatInterval(hours: number) {
|
||||
if (hours < 24) return `Cada ${hours}h`
|
||||
if (hours === 24) return 'Diario'
|
||||
if (hours === 48) return 'Cada 2 dias'
|
||||
if (hours === 168) return 'Semanal'
|
||||
return `Cada ${Math.round(hours / 24)} dias`
|
||||
}
|
||||
|
||||
function timeAgo(date: string) {
|
||||
const diff = Date.now() - new Date(date).getTime()
|
||||
const mins = Math.floor(diff / 60000)
|
||||
if (mins < 60) return `hace ${mins}min`
|
||||
const hours = Math.floor(mins / 60)
|
||||
if (hours < 24) return `hace ${hours}h`
|
||||
const days = Math.floor(hours / 24)
|
||||
return `hace ${days}d`
|
||||
}
|
||||
|
||||
function timeUntil(date: string) {
|
||||
const diff = new Date(date).getTime() - Date.now()
|
||||
if (diff < 0) return 'pendiente'
|
||||
const mins = Math.floor(diff / 60000)
|
||||
if (mins < 60) return `en ${mins}min`
|
||||
const hours = Math.floor(mins / 60)
|
||||
if (hours < 24) return `en ${hours}h`
|
||||
const days = Math.floor(hours / 24)
|
||||
return `en ${days}d`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 p-4 hover:ring-1 hover:ring-primary-500 transition-all">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<NuxtLink :to="`/tracking/${search.id}`" class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<p class="font-semibold truncate">{{ search.name }}</p>
|
||||
<UBadge :label="statusBadge().label" :color="statusBadge().color" size="xs" />
|
||||
</div>
|
||||
<p class="text-sm text-muted mb-1">{{ search.route_summary }}</p>
|
||||
<div class="flex items-center gap-3 text-xs text-muted flex-wrap">
|
||||
<span class="flex items-center gap-1">
|
||||
<UIcon name="i-lucide-timer" class="text-xs" />
|
||||
{{ formatInterval(search.interval_hours) }}
|
||||
</span>
|
||||
<span v-if="search.run_count > 0" class="flex items-center gap-1">
|
||||
<UIcon name="i-lucide-activity" class="text-xs" />
|
||||
{{ search.run_count }} ejecucion{{ search.run_count !== 1 ? 'es' : '' }}
|
||||
</span>
|
||||
<span v-if="search.last_run_at" class="flex items-center gap-1">
|
||||
<UIcon name="i-lucide-clock" class="text-xs" />
|
||||
{{ timeAgo(search.last_run_at) }}
|
||||
</span>
|
||||
<span v-if="search.is_active && search.next_run_at" class="flex items-center gap-1">
|
||||
<UIcon name="i-lucide-calendar-clock" class="text-xs" />
|
||||
Proxima: {{ timeUntil(search.next_run_at) }}
|
||||
</span>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
|
||||
<!-- Precio actual -->
|
||||
<NuxtLink :to="`/tracking/${search.id}`" class="text-right shrink-0">
|
||||
<div v-if="search.latest_snapshot" class="mb-1">
|
||||
<p class="text-lg font-bold">{{ search.latest_snapshot.cheapest_price.toFixed(0) }}€</p>
|
||||
<p class="text-xs text-muted">{{ search.latest_snapshot.total_results }} resultados</p>
|
||||
</div>
|
||||
<p v-else class="text-sm text-muted">Sin datos</p>
|
||||
</NuxtLink>
|
||||
|
||||
<!-- Acciones -->
|
||||
<div class="flex flex-col gap-1 shrink-0">
|
||||
<UButton
|
||||
:icon="search.is_active ? 'i-lucide-pause' : 'i-lucide-play'"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
:title="search.is_active ? 'Pausar' : 'Reanudar'"
|
||||
@click="emit('toggle', search.id, !search.is_active)"
|
||||
/>
|
||||
<UButton
|
||||
icon="i-lucide-trash-2"
|
||||
color="error"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
title="Eliminar"
|
||||
@click="emit('remove', search.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user