Skip to content

Commit

Permalink
add flight search
Browse files Browse the repository at this point in the history
  • Loading branch information
its-felix committed Nov 18, 2024
1 parent 4e35328 commit afcafa8
Show file tree
Hide file tree
Showing 16 changed files with 990 additions and 12 deletions.
4 changes: 2 additions & 2 deletions go/api/data/query_option.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ func WithAll(opts ...QueryScheduleOption) QueryScheduleOption {
childs := make([]querySchedulesOptions, len(opts))

for i, opt := range opts {
err = errors.Join(err, opt(&childs[i]))
err = errors.Join(err, childs[i].apply(opt))
}

return errors.Join(err, o.and(childs))
Expand All @@ -249,7 +249,7 @@ func WithAny(opts ...QueryScheduleOption) QueryScheduleOption {
childs := make([]querySchedulesOptions, len(opts))

for i, opt := range opts {
err = errors.Join(err, opt(&childs[i]))
err = errors.Join(err, childs[i].apply(opt))
}

return errors.Join(err, o.or(childs))
Expand Down
1 change: 1 addition & 0 deletions go/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ func main() {
e.GET("/auth/oauth2/code/:issuer", authHandler.Code)

e.GET("/data/sitemap.xml", web.NewSitemapHandler(dataHandler))
e.GET("/data/airlines.json", web.NewAirlinesEndpoint(dataHandler))
e.GET("/data/airports.json", web.NewAirportsEndpoint(dataHandler))
e.GET("/data/aircraft.json", web.NewAircraftEndpoint(dataHandler))
e.GET("/data/flight/:fn", web.NewFlightNumberEndpoint(dataHandler))
Expand Down
7 changes: 7 additions & 0 deletions go/api/web/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ import (
"time"
)

func NewAirlinesEndpoint(dh *data.Handler) echo.HandlerFunc {
return func(c echo.Context) error {
airlines, err := dh.Airlines(c.Request().Context(), "")
return jsonResponse(c, airlines, err, func(v []common.AirlineIdentifier) bool { return false })
}
}

func NewAirportsEndpoint(dh *data.Handler) echo.HandlerFunc {
return func(c echo.Context) error {
airports, err := dh.Airports(c.Request().Context())
Expand Down
15 changes: 14 additions & 1 deletion go/api/web/schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,19 @@ func NewQueryFlightSchedulesEndpoint(dh *data.Handler) echo.HandlerFunc {
data.WithIgnoreCodeShares(),
}

highFrequencyFilters := 0
for k, values := range c.QueryParams() {
if len(values) < 1 {
continue
}

subOpts := make([]data.QueryScheduleOption, 0, len(values))
isHighFrequency := false

switch k {
case "airline":
isHighFrequency = true

for _, value := range values {
subOpts = append(subOpts, data.WithAirlines(common.AirlineIdentifier(value)))
}
Expand Down Expand Up @@ -69,6 +74,8 @@ func NewQueryFlightSchedulesEndpoint(dh *data.Handler) echo.HandlerFunc {
}

case "minDepartureTime":
isHighFrequency = true

minDepartureTime, err := time.Parse(time.RFC3339, values[0])
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
Expand All @@ -77,6 +84,8 @@ func NewQueryFlightSchedulesEndpoint(dh *data.Handler) echo.HandlerFunc {
subOpts = append(subOpts, data.WithMinDepartureTime(minDepartureTime))

case "maxDepartureTime":
isHighFrequency = true

maxDepartureTime, err := time.Parse(time.RFC3339, values[0])
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
Expand All @@ -87,10 +96,14 @@ func NewQueryFlightSchedulesEndpoint(dh *data.Handler) echo.HandlerFunc {

if len(subOpts) > 0 {
options = append(options, data.WithAny(subOpts...))

if isHighFrequency {
highFrequencyFilters++
}
}
}

if len(options) < 3 {
if (len(options) - highFrequencyFilters) < 3 {
return echo.NewHTTPError(http.StatusBadRequest, "too few filters")
}

Expand Down
4 changes: 3 additions & 1 deletion ui/src/components/select/aircraft-multiselect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ export interface AircraftMultiselectProps {
loading: boolean;
disabled: boolean;
onChange: (options: ReadonlyArray<string>) => void;
placeholder?: string;
}

export function AircraftMultiselect({ aircraft, selectedAircraftCodes, loading, disabled, onChange }: AircraftMultiselectProps) {
export function AircraftMultiselect({ aircraft, selectedAircraftCodes, loading, disabled, onChange, placeholder }: AircraftMultiselectProps) {
const [options, optionByAircraftCode] = useMemo(() => {
const optionByAircraftCode: Record<string, MultiselectProps.Option> = {};
const otherOptions: Array<MultiselectProps.Option> = [];
Expand Down Expand Up @@ -85,6 +86,7 @@ export function AircraftMultiselect({ aircraft, selectedAircraftCodes, loading,
tokenLimit={2}
disabled={disabled}
statusType={loading ? 'loading' : 'finished'}
placeholder={placeholder}
/>
);
}
86 changes: 86 additions & 0 deletions ui/src/components/select/aircraft-select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Aircraft } from '../../lib/api/api.model';
import React, { useMemo } from 'react';
import { Select, SelectProps } from '@cloudscape-design/components';

export interface AircraftSelectProps {
aircraft: ReadonlyArray<Aircraft>;
selectedAircraftCode: string | null;
loading: boolean;
disabled: boolean;
onChange: (v: string | null) => void;
placeholder?: string;
}

export function AircraftSelect({ aircraft, selectedAircraftCode, loading, disabled, onChange, placeholder }: AircraftSelectProps) {
const [options, optionByAircraftCode] = useMemo(() => {
const optionByAircraftCode: Record<string, SelectProps.Option> = {};
const otherOptions: Array<SelectProps.Option> = [];
const groups: ReadonlyArray<{ name: string, options: Array<SelectProps.Option> }> = [
{ name: 'Airbus', options: [] },
{ name: 'Boeing', options: [] },
{ name: 'Embraer', options: [] },
{ name: 'BAE Systems', options: [] },
{ name: 'Antonov', options: [] },
{ name: 'Bombardier', options: [] },
{ name: 'Tupolev', options: [] },
];

for (const a of aircraft) {
const option = {
label: a.name,
value: a.code,
description: a.equipCode,
} satisfies SelectProps.Option;

let addedToGroup = false;
for (const group of groups) {
if (a.name.toLowerCase().includes(group.name.toLowerCase())) {
group.options.push(option);
addedToGroup = true;
break;
}
}

if (!addedToGroup) {
otherOptions.push(option);
}

optionByAircraftCode[a.code] = option;
}

const options: Array<SelectProps.Option | SelectProps.OptionGroup> = [];
for (const group of groups) {
if (group.options.length > 0) {
options.push({
label: group.name,
options: group.options,
});
}
}

options.push(...otherOptions);

return [options, optionByAircraftCode];
}, [aircraft]);

const selectedOption = useMemo(() => {
if (!selectedAircraftCode) {
return null;
}

return optionByAircraftCode[selectedAircraftCode];
}, [optionByAircraftCode, selectedAircraftCode]);

return (
<Select
options={options}
selectedOption={selectedOption}
onChange={(e) => onChange(e.detail.selectedOption?.value ?? null)}
virtualScroll={true}
filteringType={'auto'}
disabled={disabled}
statusType={loading ? 'loading' : 'finished'}
placeholder={placeholder}
/>
);
}
56 changes: 56 additions & 0 deletions ui/src/components/select/airline-multiselect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React, { useMemo } from 'react';
import { Multiselect, MultiselectProps } from '@cloudscape-design/components';

export interface AirlineMultiselectProps {
airlines: ReadonlyArray<string>;
selectedAirlines: ReadonlyArray<string>;
loading: boolean;
disabled: boolean;
onChange: (options: ReadonlyArray<string>) => void;
placeholder?: string;
}

export function AirlineMultiselect({ airlines, selectedAirlines, loading, disabled, onChange, placeholder }: AirlineMultiselectProps) {
const [options, optionByAirline] = useMemo(() => {
const options: Array<MultiselectProps.Option> = [];
const optionByAirline: Record<string, MultiselectProps.Option> = {};

for (const airline of airlines) {
const option = {
label: airline,
value: airline,
} satisfies MultiselectProps.Option;


options.push(option);
optionByAirline[airline] = option;
}

return [options, optionByAirline];
}, [airlines]);

const selectedOptions = useMemo(() => {
const result: Array<MultiselectProps.Option> = [];
for (const airline of selectedAirlines) {
const option = optionByAirline[airline];
if (option) {
result.push(option);
}
}

return result;
}, [optionByAirline, selectedAirlines]);

return (
<Multiselect
options={options}
selectedOptions={selectedOptions}
onChange={(e) => onChange(e.detail.selectedOptions.flatMap((v) => v.value ? [v.value] : []))}
keepOpen={true}
filteringType={'auto'}
disabled={disabled}
statusType={loading ? 'loading' : 'finished'}
placeholder={placeholder}
/>
);
}
127 changes: 127 additions & 0 deletions ui/src/components/select/airport-select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { Airports } from '../../lib/api/api.model';
import React, { useMemo, useState } from 'react';
import { Select, SelectProps } from '@cloudscape-design/components';

export interface AirportSelectProps {
airports: Airports;
selectedAirportCode: string | null;
loading: boolean;
disabled: boolean;
onChange: (value: string | null) => void;
placeholder?: string;
}

export function AirportSelect({ airports, selectedAirportCode, loading, disabled, onChange, placeholder }: AirportSelectProps) {
const [filterText, setFilterText] = useState('');
const [options, optionByAirportCode] = useMemo(() => {
const options: Array<SelectProps.Option | SelectProps.OptionGroup> = [];
const optionByAirportCode: Record<string, SelectProps.Option> = {};

for (const airport of airports.airports) {
const option = {
label: airport.code,
value: airport.code,
description: airport.name,
} satisfies SelectProps.Option;

options.push(option);
optionByAirportCode[airport.code] = option;
}

for (const metroArea of airports.metropolitanAreas) {
const airportOptions: Array<SelectProps.Option> = [];

for (const airport of metroArea.airports) {
const option = {
label: airport.code,
value: airport.code,
description: airport.name,
} satisfies SelectProps.Option;

airportOptions.push(option);
optionByAirportCode[airport.code] = option;
}

options.push({
label: metroArea.code,
description: metroArea.name,
options: airportOptions,
});
}

return [options, optionByAirportCode];
}, [airports]);

const displayOptions = useMemo(() => {
const filter = filterText.trim();
if (filter === '') {
return options;
}

return filterOptions(filter.toUpperCase(), options)[0];
}, [filterText, options]);

const selectedOption = useMemo(() => {
if (!selectedAirportCode) {
return null;
}

return optionByAirportCode[selectedAirportCode];
}, [optionByAirportCode, selectedAirportCode]);

return (
<Select
options={displayOptions}
selectedOption={selectedOption}
onChange={(e) => onChange(e.detail.selectedOption?.value ?? null)}
virtualScroll={true}
disabled={disabled}
statusType={loading ? 'loading' : 'finished'}
filteringType={'manual'}
onLoadItems={(e) => setFilterText(e.detail.filteringText)}
placeholder={placeholder}
/>
);
}

function filterOptions(filter: string, options: ReadonlyArray<SelectProps.Option | SelectProps.OptionGroup>): [ReadonlyArray<SelectProps.Option | SelectProps.OptionGroup>, boolean] {
const matchByLabel: Array<SelectProps.Option | SelectProps.OptionGroup> = [];
const matchByDescription: Array<SelectProps.Option | SelectProps.OptionGroup> = [];

for (const option of options) {
const label = option.label?.toUpperCase();
const description = option.description?.toUpperCase();
let matched = false;

if (label?.includes(filter)) {
matchByLabel.push(option);
matched = true;
} else if (isOptionGroup(option)) {
const [filtered, anyMatchedByLabel] = filterOptions(filter, option.options);
if (filtered.length > 0) {
const filteredOption = {
...option,
options: filtered,
} satisfies SelectProps.OptionGroup;

if (anyMatchedByLabel) {
matchByLabel.push(filteredOption);
} else {
matchByDescription.push(filteredOption);
}

matched = true;
}
}

if (!matched && description?.includes(filter)) {
matchByDescription.push(option);
}
}

return [[...matchByLabel, ...matchByDescription], matchByLabel.length > 0];
}

function isOptionGroup(option: SelectProps.Option | SelectProps.OptionGroup): option is SelectProps.OptionGroup {
return !!(option as SelectProps.OptionGroup).options;
}
1 change: 1 addition & 0 deletions ui/src/components/sidenav/sidenav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function SideNav() {
type: 'section-group',
title: 'Tools',
items: [
{ type: 'link', text: 'Flight Search', href: useHref('/tools/flight-search') },
{ type: 'link', text: 'M&M Quick Search', href: useHref('/tools/mm-quick-search') },
{ type: 'link', text: 'Links', href: useHref('/tools/links') },
],
Expand Down
Loading

0 comments on commit afcafa8

Please sign in to comment.