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>
14 KiB
Vuelato - Frontend + Backend Supabase
Contexto
Tenemos un buscador de vuelos con un cliente API completo de Flightics (9 endpoints REST funcionando). El frontend actual tiene 3 paginas basicas. Queremos:
- Un backend Supabase/PostgreSQL que wrapee Flightics y persista datos
- Un frontend ambicioso que explote toda la API
Por que Supabase
- Persistencia: watchlists, preferencias, busquedas recientes -> entre dispositivos
- Cache inteligente: airports, countries, inspirations en Postgres (no llamar a Flightics cada vez)
- Auth: usuarios con sus datos
- Cron/Edge Functions: verificar precios de watchlist automaticamente
- Realtime: notificar bajadas de precio
- Row Level Security: cada usuario ve solo sus datos
Stack
- Nuxt 4 + @nuxt/ui v4 + Tailwind CSS v4 (ya instalado)
- @nuxtjs/supabase v2 - modulo oficial, composables auto-importados
- Supabase PostgreSQL - tablas, RLS, funciones
- Leaflet - mapa explorador
- Server routes Nitro wrapeando Flightics -> Supabase cache
Esquema de base de datos (Supabase/PostgreSQL)
Tablas de cache (datos de Flightics, publicas, sin RLS)
-- Aeropuertos, ciudades, paises, regiones (de getLocations + getCountries)
-- Se sincroniza via cron o al primer request, TTL 24h
CREATE TABLE airports (
iata TEXT PRIMARY KEY,
icao TEXT,
name TEXT NOT NULL,
lat DOUBLE PRECISION,
lon DOUBLE PRECISION,
city_id TEXT,
city_name TEXT,
country_code TEXT,
country_name TEXT,
region_slug TEXT,
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE countries (
iso_code2 TEXT PRIMARY KEY,
iso_code3 TEXT,
name_eng TEXT,
name_native TEXT,
phone_prefix TEXT,
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE regions (
slug TEXT PRIMARY KEY,
name TEXT,
name_localized TEXT,
airport_codes TEXT[], -- array de IATA codes
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Cache de inspiraciones (por aeropuerto origen, TTL 1h)
CREATE TABLE inspirations_cache (
id SERIAL PRIMARY KEY,
from_airport TEXT NOT NULL,
to_airports TEXT[] NOT NULL,
min_price NUMERIC(10,2),
min_stops INTEGER,
fetched_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(from_airport, to_airports)
);
-- Cache de multi-city inspirations (TTL 1h)
CREATE TABLE multi_city_cache (
id SERIAL PRIMARY KEY,
origin_codes TEXT[] NOT NULL,
from_airport TEXT,
stops TEXT[] NOT NULL,
min_price NUMERIC(10,2),
fetched_at TIMESTAMPTZ DEFAULT now()
);
Tablas de usuario (con RLS)
-- Perfiles de usuario (extends auth.users)
CREATE TABLE profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
home_airports TEXT[] DEFAULT '{}',
default_adults INTEGER DEFAULT 1,
default_children INTEGER DEFAULT 0,
default_infants INTEGER DEFAULT 0,
locale TEXT DEFAULT 'es',
created_at TIMESTAMPTZ DEFAULT now()
);
-- Watchlist: vuelos guardados para monitorizar precio
CREATE TABLE watchlist (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
booking_token TEXT NOT NULL,
route_summary TEXT NOT NULL, -- "MAD > NTE > MAD"
departure_code TEXT,
arrival_code TEXT,
departure_date TEXT,
original_price NUMERIC(10,2) NOT NULL,
current_price NUMERIC(10,2),
price_status TEXT DEFAULT 'saved', -- saved, available, price_up, price_down, unavailable
passengers_adult INTEGER DEFAULT 1,
passengers_child INTEGER DEFAULT 0,
passengers_infant INTEGER DEFAULT 0,
last_checked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Busquedas recientes
CREATE TABLE recent_searches (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
search_params JSONB NOT NULL, -- el payload completo de busqueda
route_summary TEXT, -- "AGP,GRX > NTE"
search_mode TEXT DEFAULT 'roundtrip',
created_at TIMESTAMPTZ DEFAULT now()
);
-- Alertas de precio (futuro: cron verifica y notifica)
CREATE TABLE price_alerts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
watchlist_id UUID REFERENCES watchlist(id) ON DELETE CASCADE,
target_price NUMERIC(10,2), -- notificar si baja de este precio
is_active BOOLEAN DEFAULT true,
last_notified_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now()
);
-- RLS policies
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE watchlist ENABLE ROW LEVEL SECURITY;
ALTER TABLE recent_searches ENABLE ROW LEVEL SECURITY;
ALTER TABLE price_alerts ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users see own profile" ON profiles FOR ALL USING (auth.uid() = id);
CREATE POLICY "Users see own watchlist" ON watchlist FOR ALL USING (auth.uid() = user_id);
CREATE POLICY "Users see own searches" ON recent_searches FOR ALL USING (auth.uid() = user_id);
CREATE POLICY "Users see own alerts" ON price_alerts FOR ALL USING (auth.uid() = user_id);
Arquitectura server routes (Nitro)
Los server routes actuan como proxy inteligente: Flightics API -> cache en Supabase -> respuesta al frontend.
Rutas existentes (refactored con cache)
| Ruta | Logica |
|---|---|
GET /api/locations |
Lee de tabla airports. Si vacia o >24h, llama a Flightics getLocations(), parsea y guarda. |
GET /api/countries |
Lee de tabla countries. Si vacia o >24h, sincroniza. |
GET /api/inspirations |
Lee de inspirations_cache si <1h. Si no, llama a Flightics, guarda en cache, retorna. |
POST /api/search |
Proxy directo a Flightics searchTrips. Sin cache (resultados son efimeros). |
POST /api/detail |
Proxy directo a Flightics. |
POST /api/check |
Proxy directo a Flightics. |
POST /api/weekend-search |
Proxy directo. |
POST /api/route-flights |
Proxy directo (datos cambian frecuentemente). |
POST /api/multi-city-inspirations |
Cache 1h en multi_city_cache, sino Flightics. |
Rutas nuevas (usuario/Supabase)
| Ruta | Logica |
|---|---|
GET /api/user/profile |
Lee profile del usuario autenticado |
PUT /api/user/profile |
Actualiza home_airports, passengers, locale |
GET /api/user/watchlist |
Lista watchlist del usuario |
POST /api/user/watchlist |
Anadir vuelo a watchlist |
DELETE /api/user/watchlist/[id] |
Eliminar de watchlist |
POST /api/user/watchlist/[id]/check |
Verificar precio: llama checkTrip, actualiza en DB |
POST /api/user/watchlist/check-all |
Verificar todos los precios del usuario |
GET /api/user/recent-searches |
Ultimas 10 busquedas |
POST /api/user/recent-searches |
Guardar busqueda reciente |
POST /api/sync/locations |
Fuerza sync de airports/countries/regions desde Flightics a Supabase |
Paginas (9)
/ - Home (discovery hub)
- Hero con SearchBar compacto inline
- Tabs rapidos: Ida y vuelta | Solo ida | Multi-ciudad | Finde | Explorar
- Inspiraciones desde home airports del usuario (profile o localStorage si no logueado)
- Carrusel multi-city inspirations
- Widget "Donde por X€?" con slider presupuesto
- Busquedas recientes del usuario (si logueado)
/auth - Login/Register
- Login con email/password (Supabase Auth)
- OAuth con Google
- Registro
- Redirect post-login
/search - Busqueda completa
- 5 modos: standard, solo ida, multi-city, finde, explorar
- AirportInput con autocomplete (datos de Supabase
airportstable) - Al buscar, guarda en
recent_searchessi logueado
/results - Resultados
- Sort: precio, duracion, salida, escalas
- Filtros: precio max, escalas, hora, aerolineas
- Polling progresivo
- Vista lista + compacta
- Estrella watchlist (requiere login, o prompt to login)
/detail/[token] - Detalle
- Timeline itinerario
- Verificar precio
- Vuelos relacionados (getRouteFlights)
- Compartir
- Watchlist toggle
- CTA booking
/explore - Mapa explorador
- Mapa Leaflet con aeropuertos de tabla
airports(lat/lon) - Seleccionar origen -> inspiraciones como arcos
- Slider presupuesto
- Click destino -> popup
- Toggle directos
/route/[from]-[to] - Explorador de ruta
- getRouteFlights sin fechas
- Todas las opciones de vuelo
- Comparativa aerolineas
/multi-city - Inspiracion multi-ciudad
- Input origenes -> getMultiCityInspirations
- Tarjetas de itinerarios
- Click -> busqueda multi-city
/watchlist - Lista seguimiento (requiere auth)
- Tabla desde Supabase
watchlist - Verificar precios individual / bulk
- Badges de estado: precio bajo (verde), subio (rojo), no disponible (gris)
- Alertas de precio (target price)
- Busquedas recientes
Componentes (mismos del plan anterior + nuevos auth)
Auth
| Componente | Proposito |
|---|---|
auth/LoginForm.vue |
Email/password + Google OAuth |
auth/UserMenu.vue |
Avatar + dropdown: perfil, watchlist, logout. O boton login si no auth. |
auth/AuthGuard.vue |
Wrapper que redirige a /auth si no logueado |
Busqueda (sin cambios del plan anterior)
search/SearchBar, search/AirportInput, search/SearchModeTabs, search/StopsConfigurator, search/MaxStopsFilter, search/BudgetSlider, search/DateRangePicker, search/StayDurationPicker, search/PassengerPicker (en popover)
Resultados
results/TripCard, results/TripCardCompact, results/ResultsToolbar, results/ResultsFilters, results/LoadingMore
Detalle
detail/ItineraryTimeline, detail/SegmentCard, detail/PriceVerifier, detail/RelatedFlights, detail/ShareButton
Mapa
map/FlightMap, map/MapControls, map/MapDestinationPopup
Inspiracion
inspiration/InspirationGrid, inspiration/MultiCityCard, inspiration/BudgetExplorer
Watchlist
watchlist/WatchlistItem, watchlist/PriceAlertSetter
Composables
Existentes (refactored)
useFlightSearch- search con polling, sort, filter, weekenduseLocations- ahora lee de Supabase (tabla airports), con fallback a APIuseInspirations- lee de cache Supabase, refresh si stale
Nuevos
useAuth- wrapper deuseSupabaseUser()+useSupabaseClient(). Login, logout, register, profile CRUD.useWatchlist- CRUD contra Supabasewatchlisttable.add,remove,checkPrice,checkAll,isWatched.useRecentSearches- ultimas busquedas del usuario desde SupabaseuseUserPreferences- lee/escribeprofilestable. Home airports, default passengers.useRouteFlights- fetch route flightsuseResultFilters- sort/filter computeds client-side
Dependencias a instalar
pnpm add @nuxtjs/supabase leaflet @vue-leaflet/vue-leaflet
Orden de implementacion
Fase 0: Setup Supabase
- Crear proyecto Supabase (o usar existente)
pnpm add @nuxtjs/supabase- Configurar modulo en nuxt.config.ts + env vars
- Ejecutar SQL para crear tablas + RLS policies
- Crear server route
POST /api/sync/locationspara poblar airports/countries/regions
Fase 1: Auth + Infraestructura
auth/LoginForm.vue+/authpageauth/UserMenu.vueen app.vue headeruseAuthcomposableuseUserPreferencescomposable (lee/escribe profiles)useLocationscomposable (lee de Supabase airports table)search/AirportInput.vuecon autocomplete
Fase 2: Busqueda completa
SearchModeTabs+ refactor SearchForm para 5 modosStopsConfigurator,MaxStopsFilter,BudgetSlider,DateRangePicker,StayDurationPickerPassengerPickeren popover/searchpageuseRecentSearches+ guardar busquedas en Supabase
Fase 3: Resultados mejorados
useResultFilterscomposable- Refactor
useFlightSearchcon polling + sort/filter ResultsToolbar+ResultsFiltersTripCardmejorado +TripCardCompactLoadingMorepolling indicator
Fase 4: Detalle + Watchlist
/detail/[token]ruta dinamicaItineraryTimeline+SegmentCardPriceVerifieruseWatchlistcomposable (Supabase CRUD)- Watchlist toggle en TripCard y detail
/watchlistpage con verificacion de precios- Server routes:
/api/user/watchlist/*
Fase 5: Discovery
pnpm add leaflet @vue-leaflet/vue-leaflet/explore+FlightMap+ controles/route/[from]-[to]+useRouteFlights/multi-city+MultiCityCardRelatedFlightsen detail page
Fase 6: Home + Polish
- Home refactored: SearchBar compacto, inspiraciones, multi-city carousel, BudgetExplorer, busquedas recientes
- Navegacion actualizada en app.vue (Buscar, Explorar, Multi-city, Watchlist)
- Cache de inspiraciones en Supabase
- Responsive polish
Configuracion Nuxt
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/eslint', '@nuxt/ui', '@nuxtjs/supabase'],
supabase: {
redirectOptions: {
login: '/auth',
callback: '/auth/confirm',
exclude: ['/', '/search', '/results', '/explore', '/route/*', '/multi-city'],
}
},
// ...
})
Las paginas publicas (home, search, results, explore, route, multi-city) funcionan sin auth. Watchlist y profile requieren login.
Verificacion
pnpm devcompila sin errores- Supabase: tablas creadas, sync de locations funciona
- Auth: registro, login, logout, profile update
- Home: inspiraciones, multi-city carousel, budget explorer, busquedas recientes
/search: 5 modos, autocomplete de aeropuerto lee de Supabase/results: polling, sort/filter, watchlist star (pide login si no auth)/detail/[token]: timeline, verificar precio, related flights, compartir/explore: mapa con aeropuertos, arcos de precio, slider presupuesto/route/MAD-NTE: todos los vuelos sin fechas/multi-city: itinerarios sugeridos/watchlist: CRUD, verificar precios, badges estado- Mobile responsive en todas las paginas