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:
393
plans/vuelato-plan.md
Normal file
393
plans/vuelato-plan.md
Normal file
@@ -0,0 +1,393 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user