diff --git a/web/src/app/airline/products/page.tsx b/web/src/app/airline/products/page.tsx index c62e1d1..ce86345 100644 --- a/web/src/app/airline/products/page.tsx +++ b/web/src/app/airline/products/page.tsx @@ -1,73 +1,65 @@ 'use client'; +import { useState, useEffect, Suspense } from 'react'; +import { useSearchParams } from 'next/navigation'; import { Navigation } from '@/components/ui'; import AirlineCard from '@/components/feats/airline/AirlineCard'; +import { transformProduct, type Flight, type AirlineProduct } from '@/lib/airline-utils'; -type CabinClass = 'economy' | 'premium' | 'business' | 'first'; -type FareRule = 'flexible' | 'standard' | 'basic'; +function FlightsList() { + const searchParams = useSearchParams(); + const [flights, setFlights] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); -interface Flight { - id: string; - departure: { time: string; airport: string }; - arrival: { time: string; airport: string }; - duration: string; - stops: number; - cabinClass: CabinClass; - fareRule: FareRule; - refundable: boolean; - basePrice: number; -} + useEffect(() => { + const fetchFlights = async () => { + try { + const url = new URL('/api/products', window.location.origin); + url.searchParams.set('type', 'airline'); -const genRandomFlights = (): Flight[] => { - const airports = ['JFK', 'LAX', 'ORD', 'ATL', 'DFW', 'SFO', 'SEA', 'MIA']; - const cabins: CabinClass[] = ['economy', 'premium', 'business', 'first']; - const fareRules: FareRule[] = ['flexible', 'standard', 'basic']; + const dateIndex = searchParams.get('dateIndex'); + if (dateIndex) url.searchParams.set('dateIndex', dateIndex); - return Array.from({ length: 12 }, (_, i) => { - const depHour = Math.floor(Math.random() * 24); - const arrHour = (depHour + Math.floor(Math.random() * 6) + 2) % 24; - const stops = Math.random() > 0.6 ? 0 : Math.floor(Math.random() * 2) + 1; - const cabin = cabins[Math.floor(Math.random() * cabins.length)]; - const fareRule = fareRules[Math.floor(Math.random() * fareRules.length)]; - - const basePrice = Math.floor( - (cabin === 'economy' ? 200 : cabin === 'premium' ? 400 : cabin === 'business' ? 800 : 1500) + - Math.random() * 300 - ); - - return { - id: `flt-${i}`, - departure: { - time: `${depHour.toString().padStart(2, '0')}:${Math.floor(Math.random() * 60).toString().padStart(2, '0')}`, - airport: airports[Math.floor(Math.random() * airports.length)], - }, - arrival: { - time: `${arrHour.toString().padStart(2, '0')}:${Math.floor(Math.random() * 60).toString().padStart(2, '0')}`, - airport: airports[Math.floor(Math.random() * airports.length)], - }, - duration: `${Math.floor(Math.random() * 5) + 2}h ${Math.floor(Math.random() * 60)}m`, - stops, - cabinClass: cabin, - fareRule, - refundable: Math.random() > 0.7, - basePrice, + const res = await fetch(url.toString()); + if (!res.ok) throw new Error(`Failed to fetch: ${res.status}`); + const json = await res.json(); + const transformed = json.data.map((p: AirlineProduct) => transformProduct(p)); + setFlights(transformed); + } catch (e) { + setError(e instanceof Error ? e.message : 'Failed to load products'); + console.error('[FETCH_ERROR]', e); + } finally { + setLoading(false); + } }; - }); -}; - -export default function AirlineProducts() { - const flights = genRandomFlights(); + fetchFlights(); + }, [searchParams]); return ( <> - -
-

Available Flights

+

Available Flights

+ {loading &&
Loading...
} + {error &&
{error}
} + {!loading && !error && (
{flights.map((f) => ( ))}
+ )} + + ); +} + +export default function AirlineProducts() { + return ( + <> + +
+ Loading...}> + +
); diff --git a/web/src/components/feats/airline/AirlineCard.tsx b/web/src/components/feats/airline/AirlineCard.tsx index 5bc4332..a43dfae 100644 --- a/web/src/components/feats/airline/AirlineCard.tsx +++ b/web/src/components/feats/airline/AirlineCard.tsx @@ -1,6 +1,7 @@ 'use client'; import type { EventName } from '@/lib/events'; +import type { Flight } from '@/lib/airline-utils'; import { useHoverTracking } from '@/hooks/useHoverTracking'; import PriceDisplay from '@/components/ui/PriceDisplay'; @@ -11,22 +12,6 @@ const dispatchInteraction = (eventName: EventName, productId?: string, metadata? document.dispatchEvent(e); }; -type CabinClass = 'economy' | 'premium' | 'business' | 'first'; -type FareRule = 'flexible' | 'standard' | 'basic'; - -interface Flight { - id: string; - departure: { time: string; airport: string }; - arrival: { time: string; airport: string }; - duration: string; - stops: number; - cabinClass: CabinClass; - fareRule: FareRule; - refundable: boolean; - basePrice: number; - dateIndex?: number; -} - export default function AirlineCard({ flight }: { flight: Flight }) { const durationRef = useHoverTracking({ eventName: 'hover_over_title', diff --git a/web/src/lib/airline-utils.ts b/web/src/lib/airline-utils.ts new file mode 100644 index 0000000..74a1916 --- /dev/null +++ b/web/src/lib/airline-utils.ts @@ -0,0 +1,75 @@ +export interface AirlineProduct { + id: string; + flight_type: string; + date_index: number; + metadata: { + departure: { time: string; airport: string }; + arrival: { time: string; airport: string }; + duration: string; + stops: number; + cabin_class: string; + fare_rule: string; + refundable: boolean; + total?: number; + base_price: number; + }; + availability: number; +} + +export interface Flight { + id: string; + flightType: string; + departure: { time: string; airport: string }; + arrival: { time: string; airport: string }; + duration: string; + stops: number; + cabinClass: string; + fareRule: string; + refundable: boolean; + basePrice: number; + dateIndex: number; + availability: number; +} + +const EPOCH = new Date(0); + +export const transformProduct = (p: AirlineProduct): Flight => { + const { id, flight_type, date_index, metadata, availability } = p; + + return { + id, + flightType: flight_type, + departure: metadata.departure, + arrival: metadata.arrival, + duration: metadata.duration, + stops: metadata.stops, + cabinClass: metadata.cabin_class, + fareRule: metadata.fare_rule, + refundable: metadata.refundable, + basePrice: metadata.base_price, + dateIndex: date_index, + availability, + }; +}; + +// convert date string to days from today +export const dateToDaysFromToday = (dateStr: string): number => { + const target = new Date(dateStr); + target.setHours(0, 0, 0, 0); + const today = new Date(); + today.setHours(0, 0, 0, 0); + return Math.floor((target.getTime() - today.getTime()) / 86400000); +}; + +// convert date string to date_index (days since epoch) +export const dateToIndex = (dateStr: string): number => { + const d = new Date(dateStr); + return Math.floor((d.getTime() - EPOCH.getTime()) / 86400000); +}; + +// get current date_index +export const todayIndex = (): number => { + const now = new Date(); + now.setHours(0, 0, 0, 0); + return Math.floor((now.getTime() - EPOCH.getTime()) / 86400000); +};