fyliu Claude Opus 4.6 commited on
Commit
3253e6d
·
1 Parent(s): d2d5e25

Fix price filter to work in display currency (e.g. CAD)

Browse files

The price filter compared the user's input directly against price_usd,
but displayed prices are converted to the user's chosen currency.
Typing C$187 would not filter out a C$233 flight because its USD
price (~$160) was below 187.

Now the filter converts max_price from display currency to USD via
toUsd() before comparing. Also shows the correct currency symbol
in the filter input instead of always "$".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

frontend/dist/assets/index-CNhMhsb3.js DELETED
The diff for this file is too large to render. See raw diff
 
frontend/dist/assets/index-zJXLSgGb.js ADDED
The diff for this file is too large to render. See raw diff
 
frontend/dist/index.html CHANGED
@@ -5,7 +5,7 @@
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>Flight Search</title>
8
- <script type="module" crossorigin src="/assets/index-CNhMhsb3.js"></script>
9
  <link rel="stylesheet" crossorigin href="/assets/index-B_aBvo40.css">
10
  </head>
11
  <body>
 
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>Flight Search</title>
8
+ <script type="module" crossorigin src="/assets/index-zJXLSgGb.js"></script>
9
  <link rel="stylesheet" crossorigin href="/assets/index-B_aBvo40.css">
10
  </head>
11
  <body>
frontend/src/components/results/FilterPanel.tsx CHANGED
@@ -5,9 +5,10 @@ interface Props {
5
  flights: FlightOffer[];
6
  filters: Filters;
7
  onChange: (f: Filters) => void;
 
8
  }
9
 
10
- export default function FilterPanel({ flights, filters, onChange }: Props) {
11
  // Compute available filter options from flights
12
  const airlines = useMemo(() => {
13
  const map = new Map<string, string>();
@@ -43,7 +44,7 @@ export default function FilterPanel({ flights, filters, onChange }: Props) {
43
  <div>
44
  <h3 className="mb-2 text-sm font-medium text-gray-900">Max price</h3>
45
  <div className="flex items-center gap-2">
46
- <span className="text-sm text-gray-500">$</span>
47
  <input
48
  type="number"
49
  value={filters.max_price ?? ''}
 
5
  flights: FlightOffer[];
6
  filters: Filters;
7
  onChange: (f: Filters) => void;
8
+ currencySymbol?: string;
9
  }
10
 
11
+ export default function FilterPanel({ flights, filters, onChange, currencySymbol = '$' }: Props) {
12
  // Compute available filter options from flights
13
  const airlines = useMemo(() => {
14
  const map = new Map<string, string>();
 
44
  <div>
45
  <h3 className="mb-2 text-sm font-medium text-gray-900">Max price</h3>
46
  <div className="flex items-center gap-2">
47
+ <span className="text-sm text-gray-500">{currencySymbol}</span>
48
  <input
49
  type="number"
50
  value={filters.max_price ?? ''}
frontend/src/contexts/CurrencyContext.tsx CHANGED
@@ -5,6 +5,10 @@ interface CurrencyContextValue {
5
  currency: string;
6
  setCurrency: (c: string) => void;
7
  formatPrice: (usd: number) => string;
 
 
 
 
8
  currencies: string[];
9
  symbols: Record<string, string>;
10
  }
@@ -13,6 +17,8 @@ const CurrencyContext = createContext<CurrencyContextValue>({
13
  currency: 'USD',
14
  setCurrency: () => {},
15
  formatPrice: (usd) => `$${Math.round(usd).toLocaleString()}`,
 
 
16
  currencies: ['USD'],
17
  symbols: { USD: '$' },
18
  });
@@ -49,21 +55,22 @@ export function CurrencyProvider({ children, defaultCurrency }: { children: Reac
49
  localStorage.setItem('preferred_currency', c);
50
  }
51
 
 
 
 
52
  function formatPrice(usd: number): string {
53
- const rate = rates[currency] ?? 1;
54
- const symbol = symbols[currency] ?? currency;
55
  const converted = Math.round(usd * rate);
56
- // For currencies with large values, use locale formatting
57
- if (rate > 100) {
58
- return `${symbol}${converted.toLocaleString()}`;
59
- }
60
- return `${symbol}${converted.toLocaleString()}`;
61
  }
62
 
63
  const currencies = Object.keys(rates).sort();
64
 
65
  return (
66
- <CurrencyContext.Provider value={{ currency, setCurrency, formatPrice, currencies, symbols }}>
67
  {children}
68
  </CurrencyContext.Provider>
69
  );
 
5
  currency: string;
6
  setCurrency: (c: string) => void;
7
  formatPrice: (usd: number) => string;
8
+ /** Convert a display-currency amount back to USD. */
9
+ toUsd: (displayAmount: number) => number;
10
+ /** Current currency symbol (e.g. "C$", "€"). */
11
+ symbol: string;
12
  currencies: string[];
13
  symbols: Record<string, string>;
14
  }
 
17
  currency: 'USD',
18
  setCurrency: () => {},
19
  formatPrice: (usd) => `$${Math.round(usd).toLocaleString()}`,
20
+ toUsd: (n) => n,
21
+ symbol: '$',
22
  currencies: ['USD'],
23
  symbols: { USD: '$' },
24
  });
 
55
  localStorage.setItem('preferred_currency', c);
56
  }
57
 
58
+ const rate = rates[currency] ?? 1;
59
+ const currentSymbol = symbols[currency] ?? currency;
60
+
61
  function formatPrice(usd: number): string {
 
 
62
  const converted = Math.round(usd * rate);
63
+ return `${currentSymbol}${converted.toLocaleString()}`;
64
+ }
65
+
66
+ function toUsd(displayAmount: number): number {
67
+ return displayAmount / rate;
68
  }
69
 
70
  const currencies = Object.keys(rates).sort();
71
 
72
  return (
73
+ <CurrencyContext.Provider value={{ currency, setCurrency, formatPrice, toUsd, symbol: currentSymbol, currencies, symbols }}>
74
  {children}
75
  </CurrencyContext.Provider>
76
  );
frontend/src/pages/ResultsPage.tsx CHANGED
@@ -39,7 +39,7 @@ function parseMcLegs(searchParams: URLSearchParams): MultiCityLeg[] {
39
  export default function ResultsPage() {
40
  const [searchParams, setSearchParams] = useSearchParams();
41
  const navigate = useNavigate();
42
- const { formatPrice } = useCurrency();
43
  const { outboundFlights, returnFlights, multiCityFlights, sameAirlineDiscount, loading, error, searched, search } = useFlightSearch();
44
  const [sortBy, setSortBy] = useState<SortBy>('best');
45
  const [filters, setFilters] = useState<Filters>(EMPTY_FILTERS);
@@ -160,15 +160,17 @@ export default function ResultsPage() {
160
  flights = flights.filter(f => f.stops <= filters.max_stops!);
161
  }
162
  if (filters.max_price) {
 
 
163
  flights = flights.filter(f => {
164
  // For round trips, filter by the round-trip price shown to the user
165
  if (showingReturn && selectedOutbound) {
166
- return (returnRoundTripPrices.get(f.id) ?? f.price_usd) <= filters.max_price!;
167
  }
168
  if (isRoundTrip && !showingReturn) {
169
- return (outboundMinPrices.get(f.id) ?? f.price_usd) <= filters.max_price!;
170
  }
171
- return f.price_usd <= filters.max_price!;
172
  });
173
  }
174
  if (filters.airlines && filters.airlines.length > 0) {
@@ -211,7 +213,7 @@ export default function ResultsPage() {
211
  }
212
 
213
  return flights;
214
- }, [outboundFlights, eligibleReturnFlights, currentLegFlights, showingReturn, isRoundTrip, isMultiCity, selectedOutbound, outboundMinPrices, returnRoundTripPrices, filters, sortBy]);
215
 
216
  // Split into "best" and "other" flights
217
  const bestFlights = useMemo(() => filteredFlights.filter(f => f.is_best), [filteredFlights]);
@@ -472,6 +474,7 @@ export default function ResultsPage() {
472
  flights={filterSourceFlights}
473
  filters={filters}
474
  onChange={setFilters}
 
475
  />
476
  </div>
477
  </aside>
 
39
  export default function ResultsPage() {
40
  const [searchParams, setSearchParams] = useSearchParams();
41
  const navigate = useNavigate();
42
+ const { formatPrice, toUsd, symbol: currencySymbol } = useCurrency();
43
  const { outboundFlights, returnFlights, multiCityFlights, sameAirlineDiscount, loading, error, searched, search } = useFlightSearch();
44
  const [sortBy, setSortBy] = useState<SortBy>('best');
45
  const [filters, setFilters] = useState<Filters>(EMPTY_FILTERS);
 
160
  flights = flights.filter(f => f.stops <= filters.max_stops!);
161
  }
162
  if (filters.max_price) {
163
+ // Convert display-currency amount to USD for comparison against price_usd
164
+ const maxUsd = toUsd(filters.max_price);
165
  flights = flights.filter(f => {
166
  // For round trips, filter by the round-trip price shown to the user
167
  if (showingReturn && selectedOutbound) {
168
+ return (returnRoundTripPrices.get(f.id) ?? f.price_usd) <= maxUsd;
169
  }
170
  if (isRoundTrip && !showingReturn) {
171
+ return (outboundMinPrices.get(f.id) ?? f.price_usd) <= maxUsd;
172
  }
173
+ return f.price_usd <= maxUsd;
174
  });
175
  }
176
  if (filters.airlines && filters.airlines.length > 0) {
 
213
  }
214
 
215
  return flights;
216
+ }, [outboundFlights, eligibleReturnFlights, currentLegFlights, showingReturn, isRoundTrip, isMultiCity, selectedOutbound, outboundMinPrices, returnRoundTripPrices, filters, sortBy, toUsd]);
217
 
218
  // Split into "best" and "other" flights
219
  const bestFlights = useMemo(() => filteredFlights.filter(f => f.is_best), [filteredFlights]);
 
474
  flights={filterSourceFlights}
475
  filters={filters}
476
  onChange={setFilters}
477
+ currencySymbol={currencySymbol}
478
  />
479
  </div>
480
  </aside>