# 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: 1. Un backend Supabase/PostgreSQL que wrapee Flightics y persista datos 2. 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) ```sql -- 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) ```sql -- 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 `airports` table) - Al buscar, guarda en `recent_searches` si 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, weekend - **`useLocations`** - ahora lee de Supabase (tabla airports), con fallback a API - **`useInspirations`** - lee de cache Supabase, refresh si stale ### Nuevos - **`useAuth`** - wrapper de `useSupabaseUser()` + `useSupabaseClient()`. Login, logout, register, profile CRUD. - **`useWatchlist`** - CRUD contra Supabase `watchlist` table. `add`, `remove`, `checkPrice`, `checkAll`, `isWatched`. - **`useRecentSearches`** - ultimas busquedas del usuario desde Supabase - **`useUserPreferences`** - lee/escribe `profiles` table. Home airports, default passengers. - **`useRouteFlights`** - fetch route flights - **`useResultFilters`** - sort/filter computeds client-side --- ## Dependencias a instalar ``` pnpm add @nuxtjs/supabase leaflet @vue-leaflet/vue-leaflet ``` --- ## Orden de implementacion ### Fase 0: Setup Supabase 1. Crear proyecto Supabase (o usar existente) 2. `pnpm add @nuxtjs/supabase` 3. Configurar modulo en nuxt.config.ts + env vars 4. Ejecutar SQL para crear tablas + RLS policies 5. Crear server route `POST /api/sync/locations` para poblar airports/countries/regions ### Fase 1: Auth + Infraestructura 6. `auth/LoginForm.vue` + `/auth` page 7. `auth/UserMenu.vue` en app.vue header 8. `useAuth` composable 9. `useUserPreferences` composable (lee/escribe profiles) 10. `useLocations` composable (lee de Supabase airports table) 11. `search/AirportInput.vue` con autocomplete ### Fase 2: Busqueda completa 12. `SearchModeTabs` + refactor SearchForm para 5 modos 13. `StopsConfigurator`, `MaxStopsFilter`, `BudgetSlider`, `DateRangePicker`, `StayDurationPicker` 14. `PassengerPicker` en popover 15. `/search` page 16. `useRecentSearches` + guardar busquedas en Supabase ### Fase 3: Resultados mejorados 17. `useResultFilters` composable 18. Refactor `useFlightSearch` con polling + sort/filter 19. `ResultsToolbar` + `ResultsFilters` 20. `TripCard` mejorado + `TripCardCompact` 21. `LoadingMore` polling indicator ### Fase 4: Detalle + Watchlist 22. `/detail/[token]` ruta dinamica 23. `ItineraryTimeline` + `SegmentCard` 24. `PriceVerifier` 25. `useWatchlist` composable (Supabase CRUD) 26. Watchlist toggle en TripCard y detail 27. `/watchlist` page con verificacion de precios 28. Server routes: `/api/user/watchlist/*` ### Fase 5: Discovery 29. `pnpm add leaflet @vue-leaflet/vue-leaflet` 30. `/explore` + `FlightMap` + controles 31. `/route/[from]-[to]` + `useRouteFlights` 32. `/multi-city` + `MultiCityCard` 33. `RelatedFlights` en detail page ### Fase 6: Home + Polish 34. Home refactored: SearchBar compacto, inspiraciones, multi-city carousel, BudgetExplorer, busquedas recientes 35. Navegacion actualizada en app.vue (Buscar, Explorar, Multi-city, Watchlist) 36. Cache de inspiraciones en Supabase 37. Responsive polish --- ## Configuracion Nuxt ```ts // 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 dev` compila 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