diff --git a/cdk/lib/constructs/api-lambda-construct.ts b/cdk/lib/constructs/api-lambda-construct.ts index 829b1b7..6d1d30e 100644 --- a/cdk/lib/constructs/api-lambda-construct.ts +++ b/cdk/lib/constructs/api-lambda-construct.ts @@ -58,9 +58,9 @@ export class ApiLambdaConstruct extends Construct { }); props.dataBucket.grantRead(lambda, 'processed/flights/*'); - props.dataBucket.grantRead(lambda, 'raw/LH_Public_Data/airports.json'); - props.dataBucket.grantRead(lambda, 'raw/LH_Public_Data/cities.json'); - props.dataBucket.grantRead(lambda, 'raw/LH_Public_Data/countries.json'); + props.dataBucket.grantRead(lambda, 'raw/ourairports_data/airports.csv'); + props.dataBucket.grantRead(lambda, 'raw/ourairports_data/countries.csv'); + props.dataBucket.grantRead(lambda, 'raw/ourairports_data/regions.csv'); this.functionURL = new FunctionUrl(this, 'ApiLambdaFunctionUrl', { function: lambda, diff --git a/go/api/config_lambda.go b/go/api/config_lambda.go index 01affb9..29d3724 100644 --- a/go/api/config_lambda.go +++ b/go/api/config_lambda.go @@ -8,7 +8,6 @@ import ( "errors" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" - "github.com/explore-flights/monorepo/go/api/data" "github.com/explore-flights/monorepo/go/api/search" "os" "strconv" @@ -28,20 +27,15 @@ func s3Client(ctx context.Context) (*s3.Client, error) { return s3.NewFromConfig(cfg), nil } -func dataRepo(ctx context.Context, s3c data.MinimalS3Client) (*data.Repo, error) { - dataBucket := os.Getenv("FLIGHTS_DATA_BUCKET") - if dataBucket == "" { - return nil, errors.New("env variable FLIGHTS_DATA_BUCKET required") +func dataBucket() (string, error) { + bucket := os.Getenv("FLIGHTS_DATA_BUCKET") + if bucket == "" { + return "", errors.New("env variable FLIGHTS_DATA_BUCKET required") } - return data.NewRepo(s3c, dataBucket), nil + return bucket, nil } -func flightRepo(ctx context.Context, s3c search.MinimalS3Client) (*search.FlightRepo, error) { - dataBucket := os.Getenv("FLIGHTS_DATA_BUCKET") - if dataBucket == "" { - return nil, errors.New("env variable FLIGHTS_DATA_BUCKET required") - } - - return search.NewFlightRepo(s3c, dataBucket), nil +func flightRepo(ctx context.Context, s3c search.MinimalS3Client, bucket string) (*search.FlightRepo, error) { + return search.NewFlightRepo(s3c, bucket), nil } diff --git a/go/api/config_local.go b/go/api/config_local.go index d9bf640..a2a72ad 100644 --- a/go/api/config_local.go +++ b/go/api/config_local.go @@ -4,7 +4,6 @@ package main import ( "context" - "github.com/explore-flights/monorepo/go/api/data" "github.com/explore-flights/monorepo/go/api/local" "github.com/explore-flights/monorepo/go/api/search" "os" @@ -24,10 +23,10 @@ func s3Client(ctx context.Context) (*local.S3Client, error) { return local.NewS3Client(filepath.Join(home, "Downloads", "local_s3")), nil } -func dataRepo(ctx context.Context, s3c data.MinimalS3Client) (*data.Repo, error) { - return data.NewRepo(s3c, "flights_data_bucket"), nil +func dataBucket() (string, error) { + return "flights_data_bucket", nil } -func flightRepo(ctx context.Context, s3c search.MinimalS3Client) (*search.FlightRepo, error) { - return search.NewFlightRepo(s3c, "flights_data_bucket"), nil +func flightRepo(ctx context.Context, s3c search.MinimalS3Client, bucket string) (*search.FlightRepo, error) { + return search.NewFlightRepo(s3c, bucket), nil } diff --git a/go/api/data/handler.go b/go/api/data/handler.go index 615bdbb..685b19b 100644 --- a/go/api/data/handler.go +++ b/go/api/data/handler.go @@ -1,21 +1,136 @@ package data import ( + "cmp" "context" - "github.com/explore-flights/monorepo/go/common/lufthansa" - "slices" + "encoding/csv" + "errors" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + "io" + "strings" ) -type Country struct { - Code string `json:"code"` - Name string `json:"name"` - Cities []*City `json:"cities"` +var metroAreaMapping map[string][2]string = map[string][2]string{ + // region Asia + "PEK": {"BJS", "Beijing, China"}, + "PKX": {"BJS", "Beijing, China"}, + + "CGK": {"JKT", "Jakarta, Indonesia"}, + "HLP": {"JKT", "Jakarta, Indonesia"}, + + "KIX": {"OSA", "Osaka, Japan"}, + "ITM": {"OSA", "Osaka, Japan"}, + + "CTS": {"SPK", "Sapporo, Japan"}, + "OKD": {"SPK", "Sapporo, Japan"}, + + "ICN": {"SEL", "Seoul, South Korea"}, + "GMP": {"SEL", "Seoul, South Korea"}, + + "NRT": {"TYO", "Tokyo, Japan"}, + "HND": {"TYO", "Tokyo, Japan"}, + // endregion + // region Europe + "BER": {"BER", "Berlin, Germany"}, + + "OTP": {"BUH", "Bucharest, Romania"}, + "BBU": {"BUH", "Bucharest, Romania"}, + + "BSL": {"EAP", "Basel, Switzerland & Mulhouse, France"}, + "MLH": {"EAP", "Basel, Switzerland & Mulhouse, France"}, + + "BQH": {"LON", "London, United Kingdom"}, + "LCY": {"LON", "London, United Kingdom"}, + "LGW": {"LON", "London, United Kingdom"}, + "LTN": {"LON", "London, United Kingdom"}, + "LHR": {"LON", "London, United Kingdom"}, + "ZLS": {"LON", "London, United Kingdom"}, + "QQP": {"LON", "London, United Kingdom"}, + "QQS": {"LON", "London, United Kingdom"}, + "SEN": {"LON", "London, United Kingdom"}, + "STN": {"LON", "London, United Kingdom"}, + "ZEP": {"LON", "London, United Kingdom"}, + "QQW": {"LON", "London, United Kingdom"}, + + "MXP": {"MIL", "Milan, Italy"}, + "LIN": {"MIL", "Milan, Italy"}, + + "SVO": {"MOW", "Moscow, Russia"}, + "DME": {"MOW", "Moscow, Russia"}, + "VKO": {"MOW", "Moscow, Russia"}, + + "CDG": {"PAR", "Paris, France"}, + "ORY": {"PAR", "Paris, France"}, + "LBG": {"PAR", "Paris, France"}, + + "FCO": {"ROM", "Rome, Italy"}, + "CIA": {"ROM", "Rome, Italy"}, + + "ARN": {"STO", "Stockholm, Sweden"}, + "NYO": {"STO", "Stockholm, Sweden"}, + "BMA": {"STO", "Stockholm, Sweden"}, + // endregion + // region NA + "ORD": {"CHI", "Chicago, USA"}, + "MDW": {"CHI", "Chicago, USA"}, + + "DTW": {"DTT", "Detroit, USA"}, + "YIP": {"DTT", "Detroit, USA"}, + + "IAH": {"QHO", "Houston, USA"}, + "HOU": {"QHO", "Houston, USA"}, + + "LAX": {"QLA", "Los Angeles, USA"}, + "ONT": {"QLA", "Los Angeles, USA"}, + "SNA": {"QLA", "Los Angeles, USA"}, + "BUR": {"QLA", "Los Angeles, USA"}, + + "MIA": {"QMI", "Miami, USA"}, + "FLL": {"QMI", "Miami, USA"}, + "PBI": {"QMI", "Miami, USA"}, + + "YUL": {"YMQ", "Montreal, Canada"}, + "YMY": {"YMQ", "Montreal, Canada"}, + + "JFK": {"NYC", "New York City, USA"}, + "EWR": {"NYC", "New York City, USA"}, + "LGA": {"NYC", "New York City, USA"}, + "HPN": {"NYC", "New York City, USA"}, + + "SFO": {"QSF", "San Francisco Bay Area, USA"}, + "OAK": {"QSF", "San Francisco Bay Area, USA"}, + "SJC": {"QSF", "San Francisco Bay Area, USA"}, + + "YYZ": {"YTO", "Toronto, Canada"}, + "YTZ": {"YTO", "Toronto, Canada"}, + + "IAD": {"WAS", "Washington DC, USA"}, + "DCA": {"WAS", "Washington DC, USA"}, + "BWI": {"WAS", "Washington DC, USA"}, + // endregion + // region SA + "EZE": {"BUE", "Buenos Aires, Argentina"}, + "AEP": {"BUE", "Buenos Aires, Argentina"}, + + "GIG": {"RIO", "Rio de Janeiro, Brazil"}, + "SDU": {"RIO", "Rio de Janeiro, Brazil"}, + + "GRU": {"SAO", "São Paulo, Brazil"}, + "CGH": {"SAO", "São Paulo, Brazil"}, + "VCP": {"SAO", "São Paulo, Brazil"}, + // endregion } -type City struct { - Code string `json:"code"` - Name string `json:"name"` - Airports []*Airport `json:"airports"` +type AirportsResponse struct { + Airports []Airport `json:"airports"` + MetropolitanAreas []MetropolitanArea `json:"metropolitanAreas"` +} + +type MetropolitanArea struct { + Code string `json:"code"` + Name string `json:"name"` + Airports []Airport `json:"airports"` } type Airport struct { @@ -23,138 +138,127 @@ type Airport struct { Name string `json:"name"` } -type Handler struct { - r *Repo +type MinimalS3Client interface { + GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) } -func NewHandler(r *Repo) *Handler { - return &Handler{r} +type Handler struct { + s3c MinimalS3Client + bucket string } -func (h *Handler) Locations(ctx context.Context, lang string) ([]*Country, error) { - airports, err := h.r.Airports(ctx) - if err != nil { - return nil, err +func NewHandler(s3c MinimalS3Client, bucket string) *Handler { + return &Handler{ + s3c: s3c, + bucket: bucket, } +} - airports = addMissingAirports(airports) - - citiesByCode, err := h.cities(ctx, lang) +func (h *Handler) Airports(ctx context.Context) (AirportsResponse, error) { + r, err := h.loadCsv(ctx, "airports") if err != nil { - return nil, err + return AirportsResponse{}, err } - countriesByCode, err := h.countries(ctx, lang) - if err != nil { - return nil, err + defer r.Close() + + metroAreas := make(map[string]MetropolitanArea) + resp := AirportsResponse{ + Airports: make([]Airport, 0), + MetropolitanAreas: make([]MetropolitanArea, 0), } - r := make([]*Country, 0, len(countriesByCode)) - for _, airport := range airports { - country, ok := countriesByCode[airport.CountryCode] - if !ok { - country = &Country{ - Code: airport.CountryCode, - Name: airport.CountryCode, - Cities: make([]*City, 0), + for { + row, err := r.Read() + if err != nil { + if errors.Is(err, io.EOF) { + break } - countriesByCode[airport.CountryCode] = country + return AirportsResponse{}, err } - if !slices.Contains(r, country) { - r = append(r, country) + if !strings.HasSuffix(strings.TrimSpace(row["type"]), "_airport") { + continue + } else if strings.TrimSpace(row["scheduled_service"]) != "yes" { + continue } - city, ok := citiesByCode[airport.CityCode] - if !ok { - city = &City{ - Code: airport.CityCode, - Name: airport.CityCode, - Airports: make([]*Airport, 0), - } + var airport Airport + airport.Code = strings.TrimSpace(row["iata_code"]) + airport.Name = cmp.Or(strings.TrimSpace(row["name"]), airport.Code) - citiesByCode[airport.CityCode] = city + if airport.Code == "" { + continue } - if !slices.Contains(country.Cities, city) { - country.Cities = append(country.Cities, city) + if metroAreaValues, ok := metroAreaMapping[airport.Code]; ok { + metroArea := metroAreas[metroAreaValues[0]] + metroArea.Code = metroAreaValues[0] + metroArea.Name = metroAreaValues[1] + metroArea.Airports = append(metroArea.Airports, airport) + + metroAreas[metroArea.Code] = metroArea + } else { + resp.Airports = append(resp.Airports, airport) } + } - city.Airports = append(city.Airports, &Airport{ - Code: airport.Code, - Name: findName(airport.Names.Name, lang), - }) + for _, v := range metroAreas { + resp.MetropolitanAreas = append(resp.MetropolitanAreas, v) } - return r, nil + return resp, nil } -func (h *Handler) countries(ctx context.Context, lang string) (map[string]*Country, error) { - countries, err := h.r.Countries(ctx) +func (h *Handler) loadCsv(ctx context.Context, name string) (*csvReader, error) { + resp, err := h.s3c.GetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(h.bucket), + Key: aws.String("raw/ourairports_data/" + name + ".csv"), + }) + if err != nil { return nil, err } - r := make(map[string]*Country, len(countries)) - for _, v := range countries { - r[v.CountryCode] = &Country{ - Code: v.CountryCode, - Name: findName(v.Names.Name, lang), - Cities: make([]*City, 0), - } - } + return &csvReader{ + r: csv.NewReader(resp.Body), + c: resp.Body, + }, nil +} - return r, nil +type csvReader struct { + r *csv.Reader + c io.Closer + header []string } -func (h *Handler) cities(ctx context.Context, lang string) (map[string]*City, error) { - cities, err := h.r.Cities(ctx) +func (r *csvReader) Read() (map[string]string, error) { + row, err := r.r.Read() if err != nil { return nil, err } - r := make(map[string]*City, len(cities)) - for _, v := range cities { - r[v.CityCode] = &City{ - Code: v.CityCode, - Name: findName(v.Names.Name, lang), - Airports: make([]*Airport, 0), + if r.header == nil { + r.header = make([]string, 0) + for _, v := range row { + r.header = append(r.header, v) } - } - - return r, nil -} -func findName(n []lufthansa.Name, lang string) string { - if len(n) < 1 { - return "" + row, err = r.r.Read() + if err != nil { + return nil, err + } } - r := n[0].Name - for _, v := range n { - if v.LanguageCode == lang { - return v.Name - } else if v.LanguageCode == "EN" { - r = v.Name - } + result := make(map[string]string, len(r.header)) + for i, v := range row { + result[r.header[i]] = v } - return r + return result, nil } -func addMissingAirports(airports []lufthansa.Airport) []lufthansa.Airport { - return append( - airports, - lufthansa.Airport{ - Code: "BER", - CityCode: "BER", - CountryCode: "DE", - Names: lufthansa.Names{ - Name: lufthansa.Array[lufthansa.Name]{ - {LanguageCode: "EN", Name: "Berlin/Brandenburg"}, - }, - }, - }, - ) +func (r *csvReader) Close() error { + return r.c.Close() } diff --git a/go/api/data/repo.go b/go/api/data/repo.go deleted file mode 100644 index b5f18a9..0000000 --- a/go/api/data/repo.go +++ /dev/null @@ -1,54 +0,0 @@ -package data - -import ( - "context" - "encoding/json" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/s3" - "github.com/explore-flights/monorepo/go/common/lufthansa" -) - -type MinimalS3Client interface { - GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) -} - -type Repo struct { - s3c MinimalS3Client - bucket string -} - -func NewRepo(s3c MinimalS3Client, bucket string) *Repo { - return &Repo{ - s3c: s3c, - bucket: bucket, - } -} - -func (r *Repo) Airports(ctx context.Context) ([]lufthansa.Airport, error) { - return loadJson[[]lufthansa.Airport](ctx, r.s3c, r.bucket, "raw/LH_Public_Data/airports.json") -} - -func (r *Repo) Cities(ctx context.Context) ([]lufthansa.City, error) { - return loadJson[[]lufthansa.City](ctx, r.s3c, r.bucket, "raw/LH_Public_Data/cities.json") -} - -func (r *Repo) Countries(ctx context.Context) ([]lufthansa.Country, error) { - return loadJson[[]lufthansa.Country](ctx, r.s3c, r.bucket, "raw/LH_Public_Data/countries.json") -} - -func loadJson[T any](ctx context.Context, s3c MinimalS3Client, bucket, key string) (T, error) { - resp, err := s3c.GetObject(ctx, &s3.GetObjectInput{ - Bucket: aws.String(bucket), - Key: aws.String(key), - }) - - if err != nil { - var r T - return r, err - } - - defer resp.Body.Close() - - var r T - return r, json.NewDecoder(resp.Body).Decode(&r) -} diff --git a/go/api/go.mod b/go/api/go.mod index 0773661..4d99061 100644 --- a/go/api/go.mod +++ b/go/api/go.mod @@ -41,7 +41,6 @@ require ( golang.org/x/net v0.24.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.5.0 // indirect ) replace github.com/explore-flights/monorepo/go/common v0.0.0 => ../common diff --git a/go/api/go.sum b/go/api/go.sum index 10965c9..143d46f 100644 --- a/go/api/go.sum +++ b/go/api/go.sum @@ -79,7 +79,5 @@ golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go/api/main.go b/go/api/main.go index 450a655..12ac939 100644 --- a/go/api/main.go +++ b/go/api/main.go @@ -26,18 +26,18 @@ func main() { panic(err) } - fr, err := flightRepo(ctx, s3c) + bucket, err := dataBucket() if err != nil { panic(err) } - dr, err := dataRepo(ctx, s3c) + fr, err := flightRepo(ctx, s3c, bucket) if err != nil { panic(err) } connHandler := search.NewConnectionsHandler(fr) - dataHandler := data.NewHandler(dr) + dataHandler := data.NewHandler(s3c, bucket) e := echo.New() e.GET("/api/connections/:export", func(c echo.Context) error { @@ -138,8 +138,8 @@ func main() { } }) - e.GET("/data/:lang/locations.json", func(c echo.Context) error { - locs, err := dataHandler.Locations(c.Request().Context(), c.Param("lang")) + e.GET("/data/airports.json", func(c echo.Context) error { + airports, err := dataHandler.Airports(c.Request().Context()) if err != nil { if errors.Is(err, context.DeadlineExceeded) { return echo.NewHTTPError(http.StatusRequestTimeout, err) @@ -148,7 +148,7 @@ func main() { return echo.NewHTTPError(http.StatusInternalServerError) } - return c.JSON(http.StatusOK, locs) + return c.JSON(http.StatusOK, airports) }) if err := run(ctx, e); err != nil { diff --git a/ui/src/lib/api/api.model.ts b/ui/src/lib/api/api.model.ts index 89eff07..77350d3 100644 --- a/ui/src/lib/api/api.model.ts +++ b/ui/src/lib/api/api.model.ts @@ -19,13 +19,12 @@ export interface AuthInfo { idAtIssuer: string; } -export interface Country { - code: string; - name: string; - cities: ReadonlyArray; +export interface Airports { + airports: ReadonlyArray; + metropolitanAreas: ReadonlyArray; } -export interface City { +export interface MetropolitanArea { code: string; name: string; airports: ReadonlyArray; diff --git a/ui/src/lib/api/api.ts b/ui/src/lib/api/api.ts index e067298..7c891c9 100644 --- a/ui/src/lib/api/api.ts +++ b/ui/src/lib/api/api.ts @@ -2,7 +2,7 @@ import { HTTPClient } from '../http'; import { isJsonObject, JsonType, - ApiErrorBody, Connections, Country + ApiErrorBody, Airports, Connections, } from './api.model'; const KindSuccess = 0; @@ -45,8 +45,8 @@ export type ApiResponse = SuccessResponse | ApiErrorResponse | ErrorRes export class ApiClient { constructor(private readonly httpClient: HTTPClient) {} - getLocations(): Promise>> { - return transform(this.httpClient.fetch('/data/EN/locations.json')); + getLocations(): Promise> { + return transform(this.httpClient.fetch('/data/airports.json')); } getConnections(origins: ReadonlyArray, destinations: ReadonlyArray, minDeparture: Date, maxDeparture: Date, maxFlights: number, minLayover: number, maxLayover: number, maxDuration: number): Promise> { diff --git a/ui/src/pages/home.tsx b/ui/src/pages/home.tsx index 0226fd3..51efd46 100644 --- a/ui/src/pages/home.tsx +++ b/ui/src/pages/home.tsx @@ -35,7 +35,7 @@ import 'reactflow/dist/style.css'; import { useHttpClient } from '../components/util/context/http-client'; import { catchNotify, useAppControls } from '../components/util/context/app-controls'; import { expectSuccess } from '../lib/api/api'; -import { Connection, Connections, Country, Flight } from '../lib/api/api.model'; +import { Airports, Connection, Connections, Flight } from '../lib/api/api.model'; export function Home() { const { apiClient } = useHttpClient(); @@ -95,8 +95,11 @@ function ConnectionSearchForm({ isLoading, onSearch }: { isLoading: boolean, onS const { notification } = useAppControls(); const { apiClient } = useHttpClient(); - const [locationsLoading, setLocationsLoading] = useState(true) - const [locations, setLocations] = useState>([]); + const [airportsLoading, setAirportsLoading] = useState(true) + const [airports, setAirports] = useState({ + airports: [], + metropolitanAreas: [], + }); const [origins, setOrigins] = useState>([]); const [destinations, setDestinations] = useState>([]); @@ -108,14 +111,14 @@ function ConnectionSearchForm({ isLoading, onSearch }: { isLoading: boolean, onS const [maxDuration, setMaxDuration] = useState(60*60*26); useEffect(() => { - setLocationsLoading(true); + setAirportsLoading(true); (async () => { const { body } = expectSuccess(await apiClient.getLocations()); - setLocations(body); + setAirports(body); })() .catch(catchNotify(notification)) - .finally(() => setLocationsLoading(false)); - }, [apiClient]); + .finally(() => setAirportsLoading(false)); + }, []); function onClickSearch() { onSearch({ @@ -134,11 +137,11 @@ function ConnectionSearchForm({ isLoading, onSearch }: { isLoading: boolean, onS
Search}> - + - + @@ -432,42 +435,38 @@ function FlightNode({ data }: NodeProps) { ) } -function LocationMultiselect({ locations, loading, disabled, onChange }: { locations: ReadonlyArray, loading: boolean, disabled: boolean, onChange: (options: ReadonlyArray) => void }) { +function AirportMultiselect({ airports, loading, disabled, onChange }: { airports: Airports, loading: boolean, disabled: boolean, onChange: (options: ReadonlyArray) => void }) { const options = useMemo(() => { const options: Array = []; - for (const country of locations) { - for (const city of country.cities) { - const airportOptions: Array = []; - - for (const airport of city.airports) { - airportOptions.push({ - label: airport.code, - description: airport.name, - value: airport.code, - }); - } - - if (airportOptions.length > 0) { - if (airportOptions.length == 1) { - options.push({ - ...airportOptions[0], - filteringTags: [country.name, country.code], - }); - } else { - options.push({ - label: city.code, - description: city.name, - options: airportOptions, - filteringTags: [country.name, country.code], - }); - } - } + for (const airport of airports.airports) { + options.push({ + label: airport.code, + value: airport.code, + description: airport.name, + }); + } + + for (const metroArea of airports.metropolitanAreas) { + const airportOptions: Array = []; + + for (const airport of metroArea.airports) { + airportOptions.push({ + label: airport.code, + value: airport.code, + description: airport.name, + }); } + + options.push({ + label: metroArea.code, + description: metroArea.name, + options: airportOptions, + }); } return options; - }, [locations]); + }, [airports]); const [selectedOptions, setSelectedOptions] = useState>([]);