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>
394 lines
14 KiB
Markdown
394 lines
14 KiB
Markdown
# 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
|